The modal's scroll, made portable: thin tinted scrollbars, contained overscroll, edge shadows and a shared scroll lock.
ScrollArea wraps any scrolling region (a long list in a panel, long terms, a wide table with axis="x") and gives it the modal's scroll: thin native scrollbars in a brand tint (--scrollbar-thumb, --scrollbar-track, --scrollbar-width), overscroll-behavior: contain, and edge shadows that appear only when content has scrolled under the start edge or continues past the end (--scroll-edge-top, --scroll-edge-bottom, and -left/-right for x).
useScrollEdges(ref, { target, axis }) is the engine on its own: a passive scroll listener plus a ResizeObserver that toggle data-scrolled and data-more on the scroller (or a target such as a modal panel) without re-rendering React. Use it when your header and footer are siblings of the scroller and should cast the shadows themselves, as Modal does.
themedScrollClass is just the scrollbar and overscroll half, for a scroller you already have. useScrollLock() locks page scroll with a shared counter (nested overlays nest) and pays the scrollbar's width back as padding so nothing shifts.
Usage
import { ScrollArea, themedScrollClass, useScrollEdges, useScrollLock } from "@/components/ui";
<ScrollArea maxHeight={320} label="Workspace terms">
{terms}
</ScrollArea>Examples
Vertical
Horizontal (a wide table)
The hook on your own scroller
API
Generated from the component's TypeScript types and JSDoc (scripts/gen-ui-registry.mjs), so it always matches the source.
ScrollArea
| Prop | Type | Default | Description |
|---|---|---|---|
| maxHeight | CSSProperties["maxHeight"] | Caps the viewport; the content scrolls past it. Any CSS length. | |
| axis | "y" | "x" | "y" | "y" (default) for lists and long copy, "x" for wide tables. |
| label | string | A name for the scrolling region. When set, the viewport joins the tab order (a keyboard user can scroll it) and is announced as a region. | |
| children | ReactNode |
Also accepts the native attributes of AriaAttributes, DOMAttributes, HTMLAttributes.
Theme tokens
Per-component tokens are undeclared by default, so the default shown is the fallback the CSS uses. Set one at :root (or in theme.css) to restyle every instance. Live values are read from this page, so they follow the Rime Studio.
This component has no tokens of its own; it reads the system tokens below.
System tokens it reads
| Token | Default | Live value |
|---|---|---|
| --dur-slow | tokens.css | … |
| --ease | tokens.css | … |
| --focus-color | tokens.css | … |
| --scroll-edge-bottom | tokens.css | … |
| --scroll-edge-left | tokens.css | … |
| --scroll-edge-right | tokens.css | … |
| --scroll-edge-top | tokens.css | … |
| --scrollbar-thumb | tokens.css | … |
| --scrollbar-track | tokens.css | … |
| --scrollbar-width | tokens.css | … |
Accessibility
- Pass label to make the region focusable (tabIndex 0) and named, so keyboard users can scroll it; skip it when the content already holds focusable items.
- Native scrollbars stay: they keep platform behaviour (overlay scrollbars on macOS, high contrast in forced-colors).
Do and don't
Do
- Use it for any region that scrolls inside the page.
Don't
- Don't build a JavaScript scrollbar.
- Don't nest two vertical scrollers.
For AI agents
This page as markdown, rendered from the same registry entry. Point an agent at it (or add .md to this page's URL), or at /llms.txt for the index.
Open scroll-area.mdRelated: modal