<!-- Protocore Design System — ScrollArea -->
# ScrollArea

- **Category:** Utilities (`utilities`)
- **Slug:** `utilities/scroll-area`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { ScrollArea } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/utilities/scroll-area

> A scrollable viewport with thin, sharp, hairline scrollbars — on either or both axes, with configurable visibility.

## When to use it

Use `ScrollArea` to give a bounded region a **custom, consistent scrollbar** — one that matches the sharp, hairline PDS look on every OS instead of the browser's default. It wraps Radix's ScrollArea primitive, which keeps native wheel, touch, and keyboard scrolling intact while restyling the bar. Reach for it around scrollable panels, menus, log viewers, and long option lists. It needs a definite size on the scroll axis (a `height` for vertical, a `width` for horizontal) so the content has something to overflow. For simple page-level scrolling, don't wrap the whole document — let the native viewport handle it.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | no | — | — |
| `className` | `string` | no | — | — |
| `rootClassName` | `string` | no | — | Extra class for the outer Root element. |
| `scrollbars` | `enum` | no | `y` | Which scrollbars to render. |
| `scrollHideDelay` | `number` | no | `600` | Delay in ms before hiding the scrollbars in `scroll`/`hover` mode. |
| `style` | `CSSProperties` | no | — | — |
| `type` | `enum` | no | `hover` | When the scrollbars are shown. - `auto` — visible when the content overflows - `always` — always visible - `scroll` — visible while scrolling - `hover` — visible on hover and while scrolling |

## Examples

### Basics

Give `ScrollArea` a bounded height and its content scrolls behind a thin, sharp scrollbar. The hairline track and `--pds-border-ctrl` thumb replace the OS scrollbar for a consistent look across platforms.

```tsx
import { ScrollArea, Stack, Text } from "@protocore/pds";

export default function ScrollAreaBasics() {
  const events = Array.from({ length: 24 }, (_, i) => ({
    ts: `12:0${(i % 10).toString()}:${(i * 7) % 60}`.padEnd(8, "0"),
    msg: `relay message ${1000 + i} settled in 1 hop`,
  }));
  return (
    <ScrollArea style={{ height: 220, border: "1px solid var(--pds-border-strong)" }}>
      <Stack gap={2} style={{ padding: "var(--pds-space-4)" }}>
        {events.map((e) => (
          <Text key={e.ts} mono size="sm" color="secondary">
            {e.ts} · {e.msg}
          </Text>
        ))}
      </Stack>
    </ScrollArea>
  );
}
```

### Both axes

Set `scrollbars="both"` and give the area a bounded width and height to scroll wide content — a log table, a config dump — on both axes with a corner where the scrollbars meet.

```tsx
import { ScrollArea, Text } from "@protocore/pds";

export default function ScrollAreaBothAxes() {
  const rows = Array.from({ length: 20 }, (_, i) => i);
  return (
    <ScrollArea
      type="always"
      scrollbars="both"
      style={{ height: 200, maxWidth: 420, border: "1px solid var(--pds-border-strong)" }}
    >
      <div style={{ padding: "var(--pds-space-4)", width: 780 }}>
        <Text mono size="sm" color="secondary" style={{ whiteSpace: "pre" }}>
          {`peer_id            region        version   in_hops   out_hops   latency\n`}
          {rows
            .map(
              (i) =>
                `peer-${(9000 + i).toString(16)}   eu-central-1   v2.4.${i}    ${i % 4}         ${
                  (i + 1) % 5
                }          ${12 + i}ms`,
            )
            .join("\n")}
        </Text>
      </div>
    </ScrollArea>
  );
}
```

## Do & don't

**Do**

- Give the ScrollArea a bounded height (or width) so content can overflow and scroll.
- Use scrollbars="both" only when content genuinely overflows on both axes.
- Pick type="always" for dense tools where a hidden scrollbar would hide state.

**Don't**

- Wrap the entire page in a ScrollArea — leave document-level scrolling native.
- Rely on it without a size constraint; with no overflow there is nothing to scroll.
- Nest ScrollAreas deeply on the same axis; nested wheels trap scrolling.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Tab` | Move focus into the scrollable content. |
| `Arrow keys / Page Up / Page Down` | Scroll the focused viewport, as with a native scroll region. |

**Notes**

- Wheel, touch, and keyboard scrolling are preserved by the underlying Radix primitive — the custom bar is purely visual.
- The scrollbar thumb carries an enlarged hit target so it stays grabbable despite the thin visual width.
- Reduced-motion users get the same scrolling; only the scrollbar fade honors the OS motion preference.

## Related

`resizable-panels`, `panel`, `collapse`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
