# ScrollArea A box that scrolls, with a scrollbar that looks the same everywhere. - Group: Surfaces - Import: `import { ScrollArea } from '@misoto22/folio'` - Page: https://ui.misoto22.com/components/scroll-area/ - Related: table ## When to reach for it A bounded panel — a long option list, a log. For page-level or prose scroll the scroll-slim utility is lighter and needs no component. ## Anatomy - **Root** (required) — The bounded box, and where className lands. It is overflow-hidden and has no size of its own, so the height you give it here is the only thing that decides whether anything scrolls at all. - **Viewport** (required) — The element that actually scrolls, and the one carrying role="region", tabIndex 0 and label. Radix hides the platform’s scrollbar on it and sets the axis WITHOUT a bar to overflow: hidden. It is positioned, so an absolutely-positioned descendant travels with the content instead of hanging still over it. - **Bar** — One per orientation: an 8px track with a pill thumb at --rule-2, and touch-none — a finger scrolls the content, not the bar. It is drawn only while the pointer is inside the region and fades some 600ms after scrolling stops. - **Corner** — The square where two bars meet, which exists only at orientation="both" — the default. ## Best practices ### Do - Give it a height. With none, the root is as tall as its content, nothing ever overflows, and all the component added to the page was a keyboard stop. - Narrow orientation only when clipping the other axis is the thing you meant. Both axes scroll by default, because the axis without a bar is set to overflow: hidden — what is past that edge is not merely unmarked, it is unreachable by every key and every gesture, with the content perfectly well rendered. - Pass type="always" when the content ends flush at the boundary: the platform scrollbar is hidden and ours is not drawn until the pointer is inside, so at rest nothing on the screen says the box scrolls. ### Don’t - Do not build a drag-to-scroll affordance over it — the thumb is deliberately touch-none and the viewport is a real overflow container, so touch dragging, momentum and the wheel are already the platform’s and behave as the reader expects. - Do not nest one inside another on the same axis: the inner viewport consumes the wheel until it reaches its own end, so a reader aiming at the outer list moves the inner one instead. ## Accessibility - The viewport stays focusable. A scrollable region whose contents are not focusable has nothing to Tab to, so everything past the fold does not exist without a mouse. - label is required, because an unnamed keyboard stop announces "group" and nothing else. - Both axes scroll unless a caller narrows orientation, so content wider than the box stays reachable rather than being clipped without a bar. ## Keyboard - Tab — Moves focus into the region, which is what makes it scrollable at all without a mouse. - ↑ / ↓ / Page Up / Page Down — Scrolls it. ## ScrollArea A box that scrolls, with a scrollbar that looks the same on every platform. The reason to reach for this over `overflow-auto` is not the scrollbar — it is that Radix keeps the viewport focusable and the bar operable, which a bare overflow container does not. A scrollable region whose contents are not themselves focusable is unreachable by keyboard: there is nothing to Tab to, so everything past the fold does not exist without a mouse. For a page-level or prose scroll, the `scroll-slim` utility is lighter and needs no component. This is for a bounded panel: a long option list, a log, a sidebar that outgrows its column. Both axes scroll unless a caller says otherwise. The old vertical default read as a statement about which bar to draw and was in fact a statement about which half of the content existed: Radix sets the viewport's overflow from the mounted bars, so the axis without one was `hidden` and everything past it was unreachable — silently, and with the content perfectly well rendered. ### Props - `label` (required) — `string`. Names the region. Required, and not decoration: a scroll container is a keyboard stop, and an unnamed stop announces "group" and nothing else. - `orientation` — `'vertical' | 'horizontal' | 'both'` default `'both'`. Which axes get a bar — and therefore which axes scroll at all. Radix sets the viewport's overflow from the bars that are mounted, so an axis without one is `hidden`: what is past that edge is not merely unmarked, it is unreachable by every key and every gesture. `both` is the default for that reason. Narrow it only when clipping the other axis is the thing you meant. Also accepts: `ComponentProps`. ## Example — a bounded log ```tsx import { ScrollArea } from '@misoto22/folio' ``` ## Example — both axes ```tsx import { ScrollArea } from '@misoto22/folio' {COLUMNS.map((column) => ( ))} {ROWS.map((row) => ( {row.map((cell, index) => ( ))} ))}
Request log
{column}
{cell}
``` ## Example — a permanent bar ```tsx import { ScrollArea, Text } from '@misoto22/folio'
{SHORTCUTS.map(([keys, action]) => (
{keys}
{action}
))}
```