Scroll Area
Styled scrollbars for tight layouts—use sparingly when native scrolling is not enough.
ScrollArea wraps content that overflows and shows Experience System scrollbars. The viewport can receive keyboard focus so people can move through long content without a mouse. Vertical scrolling is built in; add a horizontal scrollbar only when you need sideways overflow. Fixed height and width on ScrollArea define the viewport; the inner block's height drives vertical scrolling.
Installation
The components are exported from @by/experience-system. Add the package with your package manager:
pnpm add @by/experience-systemIn this monorepo, depend on the workspace package (for example via workspace:* or your catalog) so imports resolve to packages/experience-system.
Composition
Use the following composition to build a ScrollArea:
ScrollArea
└── ScrollBar (optional, e.g. for horizontal overflow)ScrollArea renders the viewport, a default vertical ScrollBar, and a corner element internally—so the minimal usage is just ScrollArea with content inside. Add <ScrollBar orientation="horizontal" /> as a child when you also need horizontal scrolling. Behavior follows Radix Scroll Area.
Usage
Prefer native scrolling in general—it is usually more accessible and more consistent with platform behavior. ScrollArea is only preferred when the UI wins over UX in that trade-off: tight context menus, compact UIs, and similar surfaces where custom scrollbars match the design.
import { ScrollArea, ScrollBar } from '@by/experience-system';These are client components ('use client'). In the Next.js App Router, use them inside a client component or a dynamic import. Put ThemeProvider above the tree so reading direction flows into ScrollArea when you do not set dir yourself. Name the region for assistive technology with aria-label, aria-labelledby, or aria-describedby.
<ScrollArea
aria-label="Messages"
className="h-80 min-h-0 rounded-md border border-neutral-alpha-3"
>
<ul className="divide-y divide-neutral-alpha-3 p-2">{/* long list */}</ul>
</ScrollArea>Examples
Compact panel (menu-style)
ScrollArea is only preferred when the UI wins over UX in that trade-off: tight context menus, compact UIs, and similar surfaces where custom scrollbars match the design.
Horizontal
A wide row with whitespace-nowrap and w-max, plus <ScrollBar orientation="horizontal" />.
API Reference
Pieces map to Radix UI Scroll Area primitives. The tables below mirror the upstream API in full; the scrollbarBehavior variant, viewport tabIndex, role="region", and aria-* proxying are Experience System additions.
ScrollArea
ScrollArea is the Radix ScrollArea.Root with the Experience System viewport, a vertical ScrollBar, and a corner rendered internally. Set aria-label / aria-labelledby / aria-describedby to name the region; these are mirrored onto the viewport so screen readers describe the focusable scroll container too.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
type | enum | "hover" ("auto" | "always" | "scroll" | "hover") |
scrollHideDelay | number | 600 |
dir | enum | From ThemeProvider |
scrollbarBehavior | enum | "increasing" (experience system: "increasing" | "fixed") |
| Data attribute | Values |
|---|---|
[data-slot] | "scroll-area" (experience system) |
[role] | "region" (experience system) |
The internal viewport carries [data-slot="scroll-area-viewport"], role="group", and tabIndex={0} so keyboard users can move focus into the scrollable content.
ScrollBar
ScrollBar is the Radix ScrollArea.Scrollbar plus a thumb. The default ScrollArea already renders a vertical ScrollBar; add <ScrollBar orientation="horizontal" /> as a child when the content also overflows horizontally.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
forceMount | boolean | No default value |
orientation | "horizontal" | "vertical" | "vertical" |
scrollbarBehavior | enum | Inherits from ScrollArea (experience system: "increasing" | "fixed") |
| Data attribute | Values |
|---|---|
[data-slot] | "scroll-area-scrollbar" (experience system) |
[data-state] | "visible" | "hidden" |
[data-orientation] | "horizontal" | "vertical" |
[role] | "scrollbar" |
The thumb is rendered internally with [data-slot="scroll-area-thumb"].
Accessibility
Native scrolling is preferred over the ScrollArea component because it offers better accessibility and lets users rely on their system-level preferences and behaviors. Refer to the Usage section for specific cases where ScrollArea is appropriate.
The viewport is focusable and sits in a group; your aria-* props label the region. Scrollbars expose role="scrollbar". Even with ScrollArea, keep primary flows usable with native scrolling where you can, and add a clear label when the scrollable region is not already described by a nearby heading.
Keyboard interactions
| Key | Description |
|---|---|
Tab | Moves focus into the viewport (which is tabIndex={0}) so the scrollable region can be reached without a pointer. |
ArrowUp / ArrowDown | Scrolls vertically in small increments. |
ArrowLeft / ArrowRight | Scrolls horizontally in small increments (when a horizontal ScrollBar is present). |
PageUp / PageDown | Scrolls by one viewport. |
Home / End | Jumps to the start or end of the scroll range. |
Source in the repo: packages/experience-system/src/components/ScrollArea/ScrollArea.tsx and variants.ts. Agent-oriented contracts: packages/experience-system/src/components/ScrollArea/ScrollArea.instructions.md.