Skip to content
Folio

Surfaces

ScrollArea

A box that scrolls, with a scrollbar that looks the same everywhere.

Usage

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.
TSX
import { ScrollArea } from '@misoto22/design'

Notes

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.

Anatomy

ScrollArea anatomy
ElementDescription
RootrequiredThe 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.
ViewportrequiredThe 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.
BarOne 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.
CornerThe 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.

Examples

a bounded log

The height on the root is what makes any of this happen: with none, the box is as tall as its content, nothing overflows, and all the component added to the page was a keyboard stop. That stop is the reason to reach for this over a bare overflow-auto — Radix keeps the viewport focusable and the bar operable, so everything past the fold is reachable without a mouse. label is required for the same reason: an unnamed keyboard stop announces group and nothing else. For a page-level or prose scroll the scroll-slim utility is lighter and needs no component at all.

  • 1a2b3c4deployed
  • 1a2d2b3deployed
  • 1a2f1a2deployed
  • 1a31091rolled back
  • 1a32f80deployed
  • 1a34e6fdeployed
  • 1a36d5edeployed
  • 1a38c4ddeployed
  • 1a3ab3crolled back
  • 1a3ca2bdeployed
  • 1a3e91adeployed
  • 1a40809deployed
  • 1a426f8deployed
  • 1a445e7rolled back
  • 1a464d6deployed
  • 1a483c5deployed
  • 1a4a2b4deployed
  • 1a4c1a3deployed

both axes

orientation defaults to both, and it is written out here because the axis WITHOUT a bar is set to overflow: hidden — so narrowing this to vertical would leave content wider than the box not merely unmarked but clipped, with no key and no gesture reaching it. Narrow it only when one axis genuinely cannot overflow. The corner square only exists at both. Do not nest one of these inside another on the same axis, either: 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.

Request log
timestamplevelserviceregionrequest idmessage
2026-09-06T04:12:31Zinfoapiap-southeast-27f2a1brequest completed
2026-09-06T04:13:31Zinfoworkerap-southeast-27f49d0request completed
2026-09-06T04:14:31Zinfoworkerap-southeast-27f6985request completed
2026-09-06T04:15:31Zinfoapiap-southeast-27f893arequest completed
2026-09-06T04:16:31Zwarnworkerap-southeast-27fa8efretrying upstream after 502
2026-09-06T04:17:31Zinfoworkerap-southeast-27fc8a4request completed
2026-09-06T04:18:31Zinfoapiap-southeast-27fe859request completed
2026-09-06T04:19:31Zinfoworkerap-southeast-280080erequest completed
2026-09-06T04:20:31Zinfoworkerap-southeast-28027c3request completed
2026-09-06T04:21:31Zinfoapiap-southeast-2804778request completed
2026-09-06T04:22:31Zwarnworkerap-southeast-280672dretrying upstream after 502
2026-09-06T04:23:31Zinfoworkerap-southeast-28086e2request completed
2026-09-06T04:24:31Zinfoapiap-southeast-280a697request completed
2026-09-06T04:25:31Zinfoworkerap-southeast-280c64crequest completed

a permanent bar

type="always" draws both bars whether or not the pointer is inside. The default is hover, and the platform's own bar is hidden, so at rest there is nothing on screen saying the box scrolls — which is fine when the last row is visibly cut in half, and a trap when the content happens to end flush at the boundary, as this list nearly does. 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 the way the reader expects.

⌘K
Open the palette
⌘\
Toggle the sidebar
⌘⇧D
Deploy the current branch
⌘⇧L
Follow the logs
⌘/
Search this page
G then O
Go to overview
G then D
Go to deploys
G then S
Go to settings
?
Show every shortcut

Keyboard

ScrollArea keyboard interactions
KeyDoes
TabMoves focus into the region, which is what makes it scrollable at all without a mouse.
Page UpPage DownScrolls it.

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.