Scroll Area
A scroll container with custom, auto-hiding scrollbars — dual-axis, token-styled, replacing the native browser scrollbar.
- Status
- Since
0.1.0- Accessibility pattern
- focusable scrollable region
Last updated
Install
Add Scroll Area from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/scroll-areaUsage
import { ScrollArea } from "@/components/ui/scroll-area";
<ScrollArea className="h-72 w-full">
<div>Long, overflowing content…</div>
</ScrollArea>;Constrain the viewport with className (e.g. h-72). Without a bounded height (or width, for
horizontal) the content cannot overflow and no scrollbar appears.
Anatomy
ScrollArea is a thin wrapper over Base UI's compound ScrollArea (Root → Viewport → Scrollbar →
Thumb, plus a Corner for dual-axis). The flat ScrollArea composes those parts for you and renders
the scrollbar(s) for the chosen orientation; ScrollBar is exported separately for custom layouts.
<ScrollArea orientation="both" className="h-72 w-full">
{/* viewport content */}
</ScrollArea>ScrollArea— the root scroll container (data-slot="scroll-area"). TheclassNamesizes the scrollable viewport (data-slot="scroll-area-viewport");orientationpicks which scrollbars to render.ScrollBar— one custom scrollbar track for a single axis (data-slot="scroll-area-scrollbar",data-orientation). Auto-hides when idle, fading in on hover/scroll via Base UI'sdata-hovering/data-scrollingstate attributes. Rendered automatically; use it directly only for custom compositions.scrollbarProps— optional props passed to the automatically rendered scrollbar(s), such askeepMountedfor deterministic tests or a custom visibility policy.- The thumb (
data-slot="scroll-area-thumb") is the draggable bar —bg-border rounded-full. - The corner (
data-slot="scroll-area-corner") fills the intersection whenorientation="both".
Examples
Vertical (default)
A bounded-height list. The scrollbar sits on the right edge and fades in while you hover or scroll.
Horizontal
Set orientation="horizontal" for a row that overflows sideways; constrain the width instead of the
height.
Both axes
Set orientation="both" for content that overflows on both axes. Two scrollbars render plus a
corner (data-slot="scroll-area-corner") filling their intersection.
Orientations
vertical (default), horizontal, and both side by side — both is the one case that adds the
intersection corner and a second scrollbar.
Playground
Switch the scrollbar orientation over content that overflows on the matching axis, then copy the generated JSX.
<ScrollArea className="h-40 w-56 rounded-md border" aria-label="Changesets">
{/* overflowing content */}
</ScrollArea>API Reference
ScrollArea
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | Scrollable content. |
className | string | — | Classes for the scroll container. Set the height/width constraints here
(e.g. h-72 w-full) — without a bounded size the content cannot overflow
and no scrollbar appears. The inner viewport fills this box. |
orientation | "both" | "horizontal" | "vertical" | 'vertical' | Which scrollbar(s) to render. vertical (the default) and horizontal
each render a single axis; both renders both plus the intersection
corner for dual-axis content. |
scrollbarProps | Omit<ScrollBarProps, "orientation"> | — | Props applied to the automatically rendered scrollbar(s). Useful for tests
or custom visibility policy, e.g. keepMounted. |
Data attributes and CSS variables on ScrollArea
| Attribute | Values |
|---|---|
data-scrollable | "" |
data-slot | "scroll-area" | "scroll-area-corner" | "scroll-area-viewport" |
ScrollBar
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | 'vertical' | Which axis the scrollbar controls. |
Data attributes and CSS variables on ScrollBar
| Attribute | Values |
|---|---|
data-slot | "scroll-area-scrollbar" | "scroll-area-thumb" |
Accessibility
- The viewport is keyboard-focusable only while it can actually scroll — overflow is measured on
mount and on resize, and the viewport carries
data-scrollablewhile it is a tab stop. Keyboard users need somewhere for the arrow keys to land, but a region whose content fits would otherwise add a stop that announces nothing and does nothing. Passaria-labeloraria-labelledbytoScrollArea; the component applies it to that viewport. - Its focus ring is drawn inside the viewport (
focus-visible:-outline-offset-2). The root clips its overflow, so an outward-offset outline would be cut in half. - The focused viewport uses the global
:focus-visiblering (outline-ring) — neveroutline: none. - Scrollbars are pointer affordances only and stay out of the tab order; the native scroll keys remain fully functional, so the custom scrollbar never blocks keyboard access.
overscroll-containkeeps scroll momentum inside the area instead of chaining to the page.
| Key | Action |
|---|---|
| Tab | Move focus to the viewport (when it can scroll). |
| ↑ / ↓ | Scroll vertically by a line. |
| ← / → | Scroll horizontally by a line. |
| Page Up / Page Down | Scroll by a page. |
| Home / End | Scroll to the start / end. |
| Contract | States tested |
|---|---|
| Behaviour | default |
| Accessibility | labeled |
| Visual | default |