# Scroll Area (/ui/components/react/scroll-area)



<Playground component="scroll-area" />

## Usage [#usage]

```tsx
import { ScrollArea } from '@appica/ui-react/scroll-area'
```

```tsx
<ScrollArea className="h-60 w-64 rounded-xl border">
  <div className="flex flex-col p-2">{/* overflowing content */}</div>
</ScrollArea>
```

`ScrollArea` wraps overflowing content in a native scroll container, then paints its own slim,
themeable scrollbar on top — so scrollbars look identical across macOS, Windows, and Linux instead of
inheriting the OS chrome. It scrolls only once it has a **bounded size** and content that overflows
it: give the area a height (and/or width) via `className`, and let the content inside determine the
overflow.

It's a folded single element — you render `ScrollArea` and your content, and the viewport, scrollbars,
and thumb are wired up internally. Reach the inner scroll viewport (for a `ref`, `onScroll`, or extra
classes) through the `viewportProps` escape hatch.

## Examples [#examples]

### Vertical scrolling [#vertical-scrolling]

The default `orientation`. The area fills its parent's width and scrolls vertically once the content
is taller than the set height.

```tsx
import { ScrollArea } from '@appica/ui-react/scroll-area'

const tags = [
  'React',
  'Next.js',
  'TypeScript',
  'Tailwind CSS',
  'Base UI',
  'Motion',
  'Vitest',
  'Fumadocs',
  'pnpm',
  'Radix',
  'Zod',
  'ESLint',
  'Prettier',
  'Storybook',
  'Playwright',
  'tRPC',
]

export default function ScrollAreaDefault() {
  return (
    <ScrollArea className="border-border h-58 w-full max-w-64 rounded-lg border">
      <div className="flex flex-col p-2">
        {tags.map((tag) => (
          <div key={tag} className="text-foreground rounded-md px-3 py-2 text-sm">
            {tag}
          </div>
        ))}
      </div>
    </ScrollArea>
  )
}
```

### Horizontal scrolling [#horizontal-scrolling]

Set `orientation="horizontal"` for a row of items wider than the container — carousels, tag rows, or
code blocks. The area sizes to its content's height and scrolls sideways.

```tsx
import { ScrollArea } from '@appica/ui-react/scroll-area'

export default function ScrollAreaHorizontal() {
  return (
    <ScrollArea orientation="horizontal" className="w-full max-w-md rounded-lg border">
      <div className="flex gap-3 p-4">
        {Array.from({ length: 12 }, (_, i) => (
          <div
            key={i}
            className="bg-background-muted flex size-24 shrink-0 items-center justify-center rounded-lg text-lg font-medium"
          >
            {i + 1}
          </div>
        ))}
      </div>
    </ScrollArea>
  )
}
```

### Both axes [#both-axes]

`orientation="both"` renders a scrollbar on each axis plus a corner where they meet — handy for wide
tables or grids that overflow in two directions.

```tsx
import { ScrollArea } from '@appica/ui-react/scroll-area'

export default function ScrollAreaBoth() {
  return (
    <ScrollArea orientation="both" className="h-58 w-full max-w-md rounded-lg border">
      <div className="grid w-max grid-cols-8 gap-2 p-4">
        {Array.from({ length: 96 }, (_, i) => (
          <div
            key={i}
            className="bg-background-muted text-foreground-muted flex size-14 shrink-0 items-center justify-center rounded-md text-xs"
          >
            {i + 1}
          </div>
        ))}
      </div>
    </ScrollArea>
  )
}
```

### Scroll shadow [#scroll-shadow]

Set `scrollShadow` to fade the content at whichever edges can still scroll, hinting that there's more
beyond the fold. It's a CSS mask on the viewport, so it tracks scrolling with no runtime cost and
clears once an edge is fully reached.

```tsx
import { ScrollArea } from '@appica/ui-react/scroll-area'

const paragraphs = [
  'Scroll shadows hint that there is more content beyond the visible edge. Instead of a hard cut, the text fades out where it meets the boundary of the viewport.',
  'The fade is a CSS mask applied to the viewport, so it costs nothing at runtime and follows the content as you scroll — the top edge clears once you reach the start, and the bottom clears at the end.',
  'Because it is purely visual, the mask never blocks pointer events or selection. The text underneath stays fully interactive; only its opacity near the edges is affected.',
  'Reach for it on free-flowing prose, feeds, and long lists where a visible scrollbar alone is easy to miss. Pair it with any orientation — the mask fades whichever edges can scroll.',
  'When the content fits without overflowing, the mask resolves to fully opaque, so short content is never dimmed by accident.',
]

export default function ScrollAreaShadow() {
  return (
    <ScrollArea scrollShadow className="h-56 w-full max-w-sm">
      <div className="text-foreground flex flex-col gap-3 px-1 py-2 pr-4 text-sm leading-relaxed">
        {paragraphs.map((text, i) => (
          <p key={i}>{text}</p>
        ))}
      </div>
    </ScrollArea>
  )
}
```

### Scrollbar visibility [#scrollbar-visibility]

`scrollbarVisibility` controls when the scrollbar shows: `always` (the default), `auto` to reveal it
only while hovering or scrolling, or `never` to hide it entirely while keeping the content scrollable.
This example uses `auto` — hover the area to reveal the bar.

```tsx
import { ScrollArea } from '@appica/ui-react/scroll-area'

export default function ScrollAreaVisibility() {
  return (
    <ScrollArea scrollbarVisibility="auto" className="border-border h-58 w-full max-w-64 rounded-lg border">
      <div className="flex flex-col p-2">
        {Array.from({ length: 20 }, (_, i) => (
          <div key={i} className="text-foreground rounded-md px-3 py-2 text-sm">
            Notification {i + 1}
          </div>
        ))}
      </div>
    </ScrollArea>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a
container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your
tree in `DirectionProvider` so direction-aware behavior follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

The vertical scrollbar moves to the start (left) edge and horizontal scrolling begins from the right,
following the resolved direction. The scroll-shadow mask mirrors to fade the matching edges. For setup
details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="scroll-area" />

## API reference [#api-reference]

`ScrollArea` wraps [Base UI's Scroll Area](https://base-ui.com/react/components/scroll-area) and forwards `ref` and remaining `ScrollArea.Root` props. Its parts carry `data-slot` attributes (`scroll-area`, `scroll-area-viewport`, `scroll-area-content`, `scroll-area-scrollbar`, `scroll-area-thumb`, `scroll-area-corner`) for targeted styling.

| Prop                  | <ColMinWidth width="200">Type</ColMinWidth>         | Default      | <ColMinWidth width="220">Description</ColMinWidth>                                         |
| --------------------- | --------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------ |
| `orientation`         | <Code>'vertical' \| 'horizontal' \| 'both'</Code>   | `'vertical'` | Which axis (or axes) gets a scrollbar. `both` also renders the corner.                     |
| `scrollShadow`        | `boolean`                                           | `false`      | Fade the content at scrollable edges via a CSS mask.                                       |
| `scrollbarVisibility` | <Code>'always' \| 'auto' \| 'never'</Code>          | `'always'`   | `auto` reveals the bar on hover/scroll; `never` hides it while keeping content scrollable. |
| `viewportProps`       | <Code>Omit\<ScrollArea.Viewport, 'children'></Code> | —            | Props forwarded to the inner scroll viewport — `ref`, `onScroll`, `className`, etc.        |
| `className`           | `string`                                            | —            | Classes on the root. **Set the size here** (`h-*` / `w-*`) so the area can overflow.       |
| `children`            | `ReactNode`                                         | —            | The scrollable content.                                                                    |

## Accessibility [#accessibility]

* Built on Base UI's Scroll Area, which keeps the content in a **native scroll container** — so mouse
  wheel, trackpad, touch, and keyboard scrolling (arrows, **Page Up/Down**, **Home/End**) all work as
  users expect; only the scrollbar's appearance is custom.
* The scroll viewport is focusable so keyboard users can scroll overflowing content, with a visible
  focus ring.
* The custom scrollbar and thumb are decorative — assistive tech navigates the content directly rather
  than the bar.
* Set a `scrollbarVisibility` other than `never` when scrolling isn't otherwise discoverable; hiding
  the bar entirely can make overflow easy to miss.
