# 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<typeof ScrollAreaPrimitive.Root>`.

## Example — a bounded log

```tsx
import { ScrollArea } from '@misoto22/folio'

<ScrollArea label="Deploy log" className="h-40 w-full max-w-sm rounded-(--radius) border border-(--rule)">
  <ul className="m-0 list-none p-3 text-sm">
    {LINES.map((line) => (
      <li key={line.sha} className="flex justify-between gap-4 py-1 mono-meta text-(--ink-2)">
        <span>{line.sha}</span>
        <span className="text-(--ink-3-aa)">{line.state}</span>
      </li>
    ))}
  </ul>
</ScrollArea>
```

## Example — both axes

```tsx
import { ScrollArea } from '@misoto22/folio'

<ScrollArea
  label="Request log"
  orientation="both"
  className="h-48 w-full max-w-md rounded-(--radius) border border-(--rule)"
>
  <table className="w-max border-collapse text-start">
    <caption className="sr-only">Request log</caption>
    <thead>
      <tr>
        {COLUMNS.map((column) => (
          <th key={column} scope="col" className="whitespace-nowrap px-3 py-2 text-start eyebrow text-(--ink-3-aa)">
            {column}
          </th>
        ))}
      </tr>
    </thead>
    <tbody>
      {ROWS.map((row) => (
        <tr key={row[4]}>
          {row.map((cell, index) => (
            <td key={index} className="whitespace-nowrap px-3 py-1 mono-meta text-(--ink-2)">
              {cell}
            </td>
          ))}
        </tr>
      ))}
    </tbody>
  </table>
</ScrollArea>
```

## Example — a permanent bar

```tsx
import { ScrollArea, Text } from '@misoto22/folio'

<ScrollArea
  label="Keyboard shortcuts"
  type="always"
  className="h-44 w-full max-w-sm rounded-(--radius) border border-(--rule)"
>
  <dl className="m-0 grid grid-cols-[auto_1fr] items-baseline gap-x-4 gap-y-2 p-3">
    {SHORTCUTS.map(([keys, action]) => (
      <div key={keys} className="col-span-2 grid grid-cols-subgrid">
        <dt className="m-0 mono-meta text-(--ink)">{keys}</dt>
        <dd className="m-0">
          <Text as="span" size="sm">{action}</Text>
        </dd>
      </div>
    ))}
  </dl>
</ScrollArea>
```
