ScrollArea
Fixed-height overflow surface with a custom HUD scrollbar — a thin graphite gutter and a sharp thumb that arms to signal on hover, rendered identically across browsers (no native OS chrome). Vertical or horizontal.
"use client";import { ScrollArea } from "@/registry/okibi/ui/scroll-area";const nodes = Array.from( { length: 28 }, (_, i) => `NODE-${String(i + 1).padStart(2, "0")}`);export default function ScrollAreaDemo() { return ( <ScrollArea className="h-64 w-56 border border-border-strong bg-card"> <div className="px-4 py-3"> <div className="font-ui text-xs uppercase tracking-grid text-muted-foreground"> Active nodes </div> <ul className="mt-2 font-mono text-sm text-foreground"> {nodes.map((id) => ( <li key={id} className="border-t border-border py-1.5 first:border-t-0" > {id} </li> ))} </ul> </div> </ScrollArea> );}npx shadcn@latest add https://okibi.cndr.dev/r/scroll-area.jsonInstallation
Install the theme first, then add the component:
npx shadcn@latest add https://okibi.cndr.dev/r/scroll-area.jsonInstall the required dependencies:
npm install @radix-ui/react-scroll-areaCopy the source from the Code tab above into components/ui/scroll-area.tsx. It uses the cn helper and the okibi theme tokens — install the theme first if you haven't.
Usage
import { ScrollArea } from "@/components/ui/scroll-area"
<ScrollArea className="h-64 w-56 border border-border-strong">
<div className="p-4">{/* long content… */}</div>
</ScrollArea>Anatomy
A ScrollArea renders a focusable viewport around your content plus a ScrollBar. Add a second ScrollBar with orientation='horizontal' for sideways scroll; the horizontal child must be w-max/whitespace-nowrap to overflow.
<ScrollArea>
{/* content */}
<ScrollBar orientation="horizontal" />
</ScrollArea>Examples
Horizontal
Pair the viewport with a <ScrollBar orientation="horizontal" /> to scroll a wide row sideways — a filmstrip of sector tiles, a timeline. The gutter rotates to the bottom edge and runs the same signal-arming thumb along it.
"use client";import { ScrollArea, ScrollBar } from "@/registry/okibi/ui/scroll-area";const sectors = Array.from({ length: 12 }, (_, i) => `0x${(0x4a + i).toString(16).toUpperCase()}`);export default function ScrollAreaHorizontalDemo() { return ( <ScrollArea className="w-80 whitespace-nowrap border border-border-strong bg-card"> <div className="flex w-max gap-3 p-4"> {sectors.map((sector) => ( <figure key={sector} className="shrink-0"> <div className="flex size-28 items-center justify-center border border-border bg-surface-2 font-display text-2xl font-black text-foreground"> {sector} </div> <figcaption className="mt-2 font-ui text-[0.65rem] uppercase tracking-grid-wide text-muted-foreground"> Sector {sector} </figcaption> </figure> ))} </div> <ScrollBar orientation="horizontal" /> </ScrollArea> );}API Reference
ScrollArea
| Prop | Type | Default | Description |
|---|---|---|---|
type | "hover" | "scroll" | "auto" | "always" | "hover" | When the scrollbar is shown — hover reveals it on pointer-over, always keeps it visible. Forwarded to Radix. |
scrollHideDelay | number | 600 | Delay (ms) before the scrollbar hides after scrolling stops, for hover / scroll. Forwarded to Radix. |
ScrollBar
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "vertical" | "horizontal" | "vertical" | Gutter axis. Add a second <ScrollBar orientation="horizontal" /> inside ScrollArea for sideways scrolling. |
Accessibility
- Wraps Radix
ScrollArea; the viewport is focusable and keyboard-scrollable, with a square signal focus ring (focus-visible:outline-2) matching the HUD focus language. - Radix paints the scrollbar as real DOM elements, so it is keyboard- and pointer-operable and identical across browsers and OSes — no native gutter shift.
- Use the default
type='hover'for an overlay scrollbar;type='always'keeps it visible for surfaces where a persistent affordance matters. - Reach for this only on fixed-height panels — for ordinary page overflow, native scroll keeps find-in-page and momentum that this surface does not replace.
- The
Corneris transparent decorative chrome; the thumb arms graphite-to-signal on hover but conveys no state to assistive tech.
Guidelines
Do
- Constrain the
ScrollAreato a fixed height or width — it exists for bounded HUD panels, not the page body. - Add a horizontal
ScrollBarand give the childw-maxwhen content must scroll sideways. - Switch to
type='always'when the scrollbar should read as a permanent affordance rather than appearing on hover.
Don't
- Do not use it as a global scrollbar replacement — defer to the native
scrollbar-signalutility for page overflow. - Do not nest scroll areas inside scroll areas; pick one bounded viewport per region.
- Do not expect a horizontal thumb to appear if the child can wrap — it needs
whitespace-nowrap/w-maxto overflow.