ScrollArea

ScrollArea.Root is a div that scrolls and keeps the focus rings of what it holds whole. A scroll container clips everything past its padding box, so a ring around a control at its edge is cut. ScrollArea.Root puts its children in a content box padded by a ring's width and offset, inside any padding you give the area, so the room is always there and nothing reaches past the area's edge. When focus lands on a control whose ring would still be cut, it scrolls the rest of the ring into view.

On this page
<nav aria-label="Guide sections">
  <ScrollArea.Root style={{ blockSize: '9rem' }}>
    <Stack.Root as="ul" gap="condensed">
      <li>
        <Link.Root href="#overview">Overview</Link.Root>
      </li>
      …
    </Stack.Root>
  </ScrollArea.Root>
</nav>

axis picks the direction it scrolls in, block by default, and clips the other. Give it a size and padding the way you would any div. Lay its children out with an element inside it, since they sit in its content box. It has no server entry, so a Server Component renders it inside a client component.

A Tab stop for content with no controls

When its content overflows and nothing inside is a Tab stop, a keyboard could not reach the rest. So it then takes a Tab stop with the region role, and the arrow keys scroll it. Name it with aria-label or aria-labelledby; it warns in development when it takes a stop without a name. Its own ring is drawn inside its edge from scroll.ring.focus.

<ScrollArea.Root aria-label="Release notes" style={{ blockSize: '7rem' }}>
  <Text.Root>…</Text.Root>
</ScrollArea.Root>

Tokens now follow DTCG 2025.10 and alias with JSON Pointer. Density modes scale spacing and control heights together. Every overlay renders in place on a native dialog or popover, so it inherits the theme of the page around it. Focus rings draw from focus.ring.width and focus.ring.offset, and scroll containers leave room for them, so no ring is cut at an edge.

focusable sets the stop either way: true keeps one while it overflows even with controls inside, and false never takes one, for a list of links that focus already scrolls through.

Accessible composition

Todae handles: Room for focus rings at every edge, and scrolling so focus brings a whole ring into view. A Tab stop with the region role while the content overflows and nothing inside is a Tab stop, kept while focus is on it so a resize never drops focus, and removed once the content fits. Its own focus ring, inside its edge.

You supply: A name with aria-label or aria-labelledby whenever it can take a Tab stop. A height or width that makes it scroll.

Watch for: Layout styles such as display: grid or gap on ScrollArea.Root: its only child is its content box, so they never reach your children. Put them on an element inside it. A row of items that should keep their width when it scrolls sideways: as in any scrolling div, flex and grid items shrink to their narrowest before the row scrolls. Give them flex: none or a fixed track size.

When to use

When: A region that scrolls on its own, such as a sidebar, a list in a panel or a row of cards that scrolls sideways.

When not: The page itself scrolls → let it, with no scroll area. Content that should fold rows away rather than scroll → ChipList or maxRows on Field.ChipInput. A sheet's body → BottomSheet's BottomSheet.Body, which already scrolls and leaves the same room.

Props

ScrollArea.Root props
NameTypeDescription
axis'block' | 'inline' | 'both'

The direction it scrolls in. The other direction is clipped.

Default: 'block'

focusableboolean

Whether it takes a Tab stop, so a keyboard can scroll it, while its content overflows. Without it, only when nothing inside can take focus. Name it with aria-label or aria-labelledby when it can take one.