Scrollbar Utility
Discord-styled scrollbars for any scrollable container, with an optional keyboard-focusable viewport.
Discord-styled scrollbars for any scrollable container, with an optional keyboard-focusable viewport. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.
newt-scrollbarimport { ScrollArea } from "@/registry/bases/newt/ui/scrollbar"
const messages = Array.from({ length: 24 }, (_, i) => `Message line ${i + 1}`)
export default function ScrollbarDemo() {
return (
<ScrollArea className="max-h-[240px] w-full max-w-sm overflow-y-auto rounded-md bg-newt-bg-elevated p-3">
<ul className="flex flex-col gap-2 text-[14px] text-newt-text-secondary">
{messages.map((m) => (
<li key={m}>{m}</li>
))}
</ul>
</ScrollArea>
)
}<script setup lang="ts">
import { ScrollArea } from "~~/registry/bases/newt/ui/scrollbar"
const messages = Array.from({ length: 24 }, (_, i) => `Message line ${i + 1}`)
</script>
<template>
<ScrollArea
class="max-h-[240px] w-full max-w-sm overflow-y-auto rounded-md bg-newt-bg-elevated p-3"
>
<ul class="flex flex-col gap-2 text-[14px] text-newt-text-secondary">
<li v-for="m in messages" :key="m">{{ m }}</li>
</ul>
</ScrollArea>
</template>Installation
pnpm dlx newtui@latest add scrollbarnpx newtui@latest add scrollbaryarn dlx newtui@latest add scrollbarbunx newtui@latest add scrollbarCopy the source into your project
import * as React from "react"
import { cn } from "@/lib/utils"
/**
* Tailwind classes that give any scrollable container Discord-styled
* scrollbars (thin, rounded thumb, transparent track). Apply via
* `<ScrollArea>` or spread `scrollbarClassName` on your own element.
*/
const scrollbarClassName = [
"[scrollbar-width:thin] [scrollbar-color:var(--newt-bg-active)_transparent]",
"[&::-webkit-scrollbar]:h-4 [&::-webkit-scrollbar]:w-4",
"[&::-webkit-scrollbar-track]:bg-transparent",
"[&::-webkit-scrollbar-thumb]:rounded-lg [&::-webkit-scrollbar-thumb]:border-4 [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-newt-bg-active [&::-webkit-scrollbar-thumb]:bg-clip-padding",
"[&::-webkit-scrollbar-thumb:hover]:bg-newt-text-muted [&::-webkit-scrollbar-thumb:hover]:bg-clip-padding",
"[&::-webkit-scrollbar-corner]:bg-transparent",
].join(" ")
export interface ScrollAreaProps extends React.ComponentProps<"div"> {
/**
* Put the viewport in the tab order so it can be scrolled from the keyboard
* (WCAG 2.1.1). Off by default: a focusable region only helps when the area
* actually scrolls and holds no other focusable content.
*/
focusable?: boolean
/** Accessible name of the focusable viewport. */
viewportLabel?: string
}
const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
(
{
className,
focusable = false,
viewportLabel = "Scrollable region",
...props
},
ref
) => (
<div
ref={ref}
tabIndex={focusable ? 0 : undefined}
role={focusable ? "group" : undefined}
aria-label={focusable ? viewportLabel : undefined}
className={cn("overflow-auto", scrollbarClassName, className)}
{...props}
/>
)
)
ScrollArea.displayName = "ScrollArea"
export { ScrollArea, ScrollArea as Scrollbar, scrollbarClassName }<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { scrollbarClassName } from "."
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
/**
* Put the viewport in the tab order so it can be scrolled from the keyboard
* (WCAG 2.1.1). Off by default: a focusable region only helps when the area
* actually scrolls and holds no other focusable content.
*/
focusable?: boolean
/** Accessible name of the focusable viewport. */
viewportLabel?: string
}>(),
{ viewportLabel: "Scrollable region" }
)
</script>
<template>
<div
:tabindex="props.focusable ? 0 : undefined"
:role="props.focusable ? 'group' : undefined"
:aria-label="props.focusable ? props.viewportLabel : undefined"
:class="cn('overflow-auto', scrollbarClassName, props.class)"
>
<slot />
</div>
</template>export { default as ScrollArea } from "./ScrollArea.vue"
/**
* Tailwind classes that give any scrollable container Discord-styled
* scrollbars (thin, rounded thumb, transparent track).
*/
export const scrollbarClassName = [
"[scrollbar-width:thin] [scrollbar-color:var(--newt-bg-active)_transparent]",
"[&::-webkit-scrollbar]:h-4 [&::-webkit-scrollbar]:w-4",
"[&::-webkit-scrollbar-track]:bg-transparent",
"[&::-webkit-scrollbar-thumb]:rounded-lg [&::-webkit-scrollbar-thumb]:border-4 [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-newt-bg-active [&::-webkit-scrollbar-thumb]:bg-clip-padding",
"[&::-webkit-scrollbar-thumb:hover]:bg-newt-text-muted [&::-webkit-scrollbar-thumb:hover]:bg-clip-padding",
"[&::-webkit-scrollbar-corner]:bg-transparent",
].join(" ")Update the import paths
The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.
Usage
import { Scrollbar } from "@/components/ui/scrollbar"
export function Example() {
return <Scrollbar />
}<script setup lang="ts">
import { ScrollArea } from "@/components/ui/scrollbar"
</script>
<template>
<ScrollArea />
</template>API reference
ScrollArea
focusable?boolean | undefined
Put the viewport in the tab order so it can be scrolled from the keyboard (WCAG 2.1.1). Off by default: a focusable region only helps when the area actually scrolls and holds no other focusable content.
boolean | undefinedviewportLabel?string | undefined
Accessible name of the focusable viewport.
string | undefinedThe registry exports ScrollArea, Scrollbar. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.
Accessibility
Preserve the native semantics of the rendered elements and pass through ARIA attributes when the surrounding context needs a label or description. Any interactive child needs a visible focus indicator, and meaning must not rely on colour alone.
Tokens
Scrollbar Utility reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer. In the plain HTML and CSS distribution the same component is newt-scrollbar.