# Scroll Area

A container that scrolls its content and shows a thin scrollbar in the colors of the theme.

```tsx
import { ScrollArea } from 'ferry-ui'

const ENTRIES = [
  'Maya Chen invited Jonas Weber',
  'Invoice INV-2041 paid',
  'Jonas Weber joined the workspace',
  'API key "Production backend" created',
  'Project Atlas renamed to Beacon',
  'Priya Patel changed the plan to Pro',
  'Invoice INV-2042 sent to Acme',
  'Order ORD-10482 shipped',
  'Maya Chen changed the role of Lucas Martin',
  'API key "Staging" revoked',
  'Invoice INV-2043 is overdue',
  'Order ORD-10483 refunded',
  'Lucas Martin left the workspace',
  'Project Compass archived',
]

export default function ScrollAreaHero() {
  return (
    <ScrollArea
      className="h-64 w-full max-w-sm rounded-lg border bg-surface-100"
      viewportProps={{ tabIndex: 0, role: 'region', 'aria-label': 'Activity log' }}
    >
      <ul className="divide-y text-[13px] text-foreground-light">
        {ENTRIES.map((entry) => (
          <li key={entry} className="px-4 py-2.5">
            {entry}
          </li>
        ))}
      </ul>
    </ScrollArea>
  )
}
```

## Usage guidelines

- **A region with a fixed size.** Use a scroll area for a long menu, a side panel, a log or a list in a card.
- **Give the area a limit.** Set a height, such as `h-72`. With no limit, the area grows with its content and does not scroll.
- **The page needs no scroll area.** Keep the scrollbar of the browser for the main scroll of the page.
- **A table has its own scroll.** A [Table](/docs/components/table) scrolls in its own container.

## Anatomy

Import the parts. `ScrollArea` already has a vertical scrollbar. Add a `ScrollBar` only for a horizontal scroll.

```tsx title="Anatomy"

<ScrollArea>
  {/* the content */}
  <ScrollBar orientation="horizontal" />
</ScrollArea>
```

## Examples

### Scrollbar always in view

By default, the scrollbar shows while the pointer is on the area. Set `type="always"` to keep the scrollbar in view.

```tsx
import { ScrollArea, Separator } from 'ferry-ui'

const VERSIONS = Array.from({ length: 24 }, (_, index) => `v2.${24 - index}.0`)

export default function ScrollAreaAlways() {
  return (
    <ScrollArea
      type="always"
      className="h-56 w-56 rounded-lg border bg-surface-100"
      viewportProps={{ tabIndex: 0, role: 'region', 'aria-label': 'Releases' }}
    >
      <div className="p-4">
        <div className="mb-3 mono-label">Releases</div>
        {VERSIONS.map((version) => (
          <div key={version}>
            <div className="py-1.5 font-mono text-[13px] text-foreground-light">{version}</div>
            <Separator />
          </div>
        ))}
      </div>
    </ScrollArea>
  )
}
```

### Horizontal scroll

Add a `ScrollBar` with `orientation="horizontal"` as the last child. The area then scrolls content that is wider than it.

```tsx
import { ScrollArea, ScrollBar } from 'ferry-ui'

const PROJECTS = [
  { name: 'Marketing site', tasks: 14 },
  { name: 'Billing portal', tasks: 8 },
  { name: 'Mobile app', tasks: 23 },
  { name: 'Design system', tasks: 5 },
  { name: 'Help center', tasks: 11 },
  { name: 'Partner API', tasks: 9 },
]

export default function ScrollAreaHorizontal() {
  return (
    <ScrollArea
      type="always"
      className="w-full max-w-md rounded-lg border whitespace-nowrap"
      viewportProps={{ tabIndex: 0, role: 'region', 'aria-label': 'Projects' }}
    >
      {/* w-max: the row keeps the width of its items, wider than the area. */}
      <div className="flex w-max gap-3 p-4">
        {PROJECTS.map((project) => (
          <div key={project.name} className="w-40 shrink-0 rounded-lg border bg-surface-100 p-3">
            <div className="truncate text-sm font-medium text-foreground">{project.name}</div>
            <div className="mt-1 text-xs text-foreground-lighter">
              <span className="tabular">{project.tasks}</span> open tasks
            </div>
          </div>
        ))}
      </div>
      <ScrollBar orientation="horizontal" />
    </ScrollArea>
  )
}
```

### Height in a flex column

In a flex column, give the area the `min-h-0 flex-1` classes. The area takes the height that the other elements leave.

```tsx
import { ScrollArea } from 'ferry-ui'

const NOTIFICATIONS = [
  { who: 'Maya Chen', what: 'added a comment on the invoice INV-2041', when: '2 min' },
  { who: 'Jonas Weber', what: 'invited you to the project Beacon', when: '14 min' },
  { who: 'Priya Patel', what: 'marked the invoice INV-2042 as paid', when: '1 h' },
  { who: 'Lucas Martin', what: 'gave you the task "Update the emails"', when: '3 h' },
  { who: 'Maya Chen', what: 'created a new API key', when: '5 h' },
  { who: 'Jonas Weber', what: 'changed the plan to Pro', when: 'Yesterday' },
  { who: 'Priya Patel', what: 'closed 6 tasks in the project Atlas', when: 'Yesterday' },
  { who: 'Lucas Martin', what: 'exported the orders of September', when: '2 d' },
]

export default function ScrollAreaPanel() {
  return (
    <div className="flex h-80 w-full max-w-xs flex-col overflow-hidden rounded-lg border bg-surface-100">
      {/* The header keeps its height. */}
      <div className="flex h-11 shrink-0 items-center justify-between border-b px-4">
        <span className="text-sm font-medium text-foreground">Notifications</span>
        <span className="tabular text-xs text-foreground-lighter">{NOTIFICATIONS.length} new</span>
      </div>
      {/* The area takes the height that stays free. */}
      <ScrollArea
        className="min-h-0 flex-1"
        viewportProps={{ tabIndex: 0, role: 'region', 'aria-label': 'Notifications' }}
      >
        <ul className="divide-y">
          {NOTIFICATIONS.map((notification) => (
            <li key={notification.what} className="flex flex-col gap-0.5 px-4 py-2.5">
              <span className="text-[13px] text-foreground-light">
                <span className="font-medium text-foreground">{notification.who}</span> {notification.what}
              </span>
              <span className="text-xs text-foreground-lighter">{notification.when}</span>
            </li>
          ))}
        </ul>
      </ScrollArea>
    </div>
  )
}
```

## Accessibility

- Content with no link, button or field cannot take the focus. For such content, pass `tabIndex`, `role` and `aria-label` in `viewportProps`.
- <Kbd>Tab</Kbd> then moves the focus to the area. The arrow keys scroll it.
- If the content has links, buttons or fields, do not set `viewportProps`.

```tsx
<ScrollArea className="h-72" viewportProps={{ tabIndex: 0, role: 'region', 'aria-label': 'Activity log' }}>
  {/* text or a list with no control */}
</ScrollArea>
```

## API reference

`ScrollArea` and `ScrollBar` also accept the props of their Radix UI primitive.

### ScrollArea

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `viewportProps` | `(ScrollAreaViewportProps & RefAttributes<HTMLDivElement>)` |  | Props for the scrolling viewport element (its `className` is merged). When the area holds no focusable content (plain text, a read-only list), make the viewport itself focusable so it can be scrolled from the keyboard in every browser: `viewportProps={{ tabIndex: 0, role: "region", "aria-label": "Activity" }}`. Leave it unset when the content has links, buttons or fields. |
| `type` | `"auto" \| "always" \| "scroll" \| "hover"` |  |  |
| `dir` | `"ltr" \| "rtl"` |  |  |
| `scrollHideDelay` | `number` |  |  |
| `asChild` | `boolean` |  |  |

### ScrollBar

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `forceMount` | `true` |  |  |
| `orientation` | `"horizontal" \| "vertical"` | `vertical` |  |
| `asChild` | `boolean` |  |  |
