# Scroll Area

Constrain overflowing content with styled scrollbars.

Scrollable list of release tags

[Complete TSX file](/examples/scroll-area/default.tsx)

```tsx
"use client";

import { ScrollArea } from "@/siteplane/components/scroll-area";

const tags = Array.from(
  { length: 50 },
  (_, index) => `v1.2.0-beta.${50 - index}`,
);

export default function Example() {
  return (
    <div className="h-72 w-48 overflow-hidden rounded-lg border">
      <ScrollArea aria-label="Tags">
        <div className="p-4">
          <h3 className="mb-4 font-medium text-sm leading-none">Tags</h3>
          <div className="flex flex-col gap-2">
            {tags.map((tag) => (
              <div className="text-sm" key={tag}>
                {tag}
              </div>
            ))}
          </div>
        </div>
      </ScrollArea>
    </div>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/scroll-area
```

With npm: `npx shadcn@4.18.0 add @siteplane/scroll-area`. [Registry JSON](/registry/scroll-area.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/badge
```

## Usage

```tsx
import { ScrollArea } from "@/siteplane/components/scroll-area";

<div className="h-40">
  <ScrollArea>Content</ScrollArea>
</div>;
```

## Fade the edges

Set `scrollFade` to fade the edges of the viewport where more content waits. The top fades once the content is scrolled down, the bottom while more follows, and the left and right edges do the same. An edge with nothing beyond it stays sharp, and the fade grows with the distance left to scroll, up to 1.5rem. It is off by default on `ScrollArea` itself. `SidebarContent` and the lists of Combobox and Autocomplete always fade, and the panels of Dialog, Sheet and Drawer fade unless you pass `scrollFade={false}`.

List with faded edges

[Complete TSX file](/examples/scroll-area/fade.tsx)

```tsx
"use client";

import { ScrollArea } from "@/siteplane/components/scroll-area";

const activity = [
  {
    id: 1,
    text: "Ben Carter commented on Website redesign",
    time: "2 min ago",
  },
  {
    id: 2,
    text: "Jonas Weber closed Fix invoice rounding",
    time: "14 min ago",
  },
  { id: 3, text: "Priya Shah uploaded brand-guidelines.pdf", time: "1 h ago" },
  { id: 4, text: "Kai Fischer moved Mobile app to Review", time: "2 h ago" },
  { id: 5, text: "Sofia Nguyen invited two teammates", time: "3 h ago" },
  { id: 6, text: "Ben Carter archived Q2 planning", time: "5 h ago" },
  { id: 7, text: "Jonas Weber opened Billing export bug", time: "Yesterday" },
  { id: 8, text: "Priya Shah joined the Design team", time: "Yesterday" },
  { id: 9, text: "Kai Fischer renamed Data migration", time: "2 days ago" },
  {
    id: 10,
    text: "Sofia Nguyen changed the workspace logo",
    time: "3 days ago",
  },
];

export default function Example() {
  return (
    <div className="h-64 w-full max-w-md overflow-hidden rounded-lg border">
      <ScrollArea aria-label="Recent activity" scrollFade>
        <ul className="divide-y px-4">
          {activity.map((item) => (
            <li
              className="flex items-baseline justify-between gap-4 py-3 text-sm"
              key={item.id}
            >
              <span>{item.text}</span>
              <span className="shrink-0 text-muted-foreground text-xs">
                {item.time}
              </span>
            </li>
          ))}
        </ul>
      </ScrollArea>
    </div>
  );
}
```

## Horizontal scrolling

Give the content an intrinsic width, such as `w-max`, so it scrolls horizontally. `scrollbarGutter` reserves space so the scrollbar doesn't cover the content, and `scrollFade` fades the edge that still has content beyond it.

Horizontally scrolling tags

[Complete TSX file](/examples/scroll-area/tag-row.tsx)

```tsx
"use client";

import { Badge } from "@/siteplane/components/badge";
import { ScrollArea } from "@/siteplane/components/scroll-area";

const topics = [
  "Design",
  "Frontend",
  "Backend",
  "Infrastructure",
  "Research",
  "Billing",
  "Support",
  "Growth",
  "Security",
  "Analytics",
];

export default function Example() {
  return (
    <div className="w-full max-w-96 overflow-hidden rounded-lg border">
      <ScrollArea aria-label="Topics" scrollFade scrollbarGutter>
        <div className="flex w-max gap-2 p-4">
          {topics.map((topic) => (
            <Badge key={topic} variant="outline">
              {topic}
            </Badge>
          ))}
        </div>
      </ScrollArea>
    </div>
  );
}
```

## Both axes

Content that is wider and taller than the viewport scrolls in both directions. With `scrollFade`, each edge fades on its own.

Content scrolling in both directions

[Complete TSX file](/examples/scroll-area/both-axes.tsx)

```tsx
"use client";

import { ScrollArea } from "@/siteplane/components/scroll-area";

export default function Example() {
  return (
    <div className="h-64 w-full max-w-80 overflow-hidden rounded-lg border">
      <ScrollArea aria-label="Release notes" scrollFade>
        <div className="flex w-[36rem] flex-col gap-3 p-4 text-sm">
          <h3 className="font-medium">Release notes</h3>
          <p>
            Projects can now be archived from the project list. Archived
            projects keep their history and files and can be restored at any
            time by a workspace owner.
          </p>
          <p>
            Invoices include a line for every seat that was active during the
            billing period. Seats added in the middle of a period are charged
            proportionally on the next invoice.
          </p>
          <p>
            Search now matches project descriptions and comments in addition to
            project names. Results are grouped by project and sorted by the most
            recent activity.
          </p>
          <p>
            Workspace owners can require two-factor authentication for all
            members. Members without it are asked to set it up the next time
            they sign in.
          </p>
        </div>
      </ScrollArea>
    </div>
  );
}
```

## Fill the viewport

Set `fill` when the content is a flex column that should stretch to the viewport height, for example to pin a footer with `mt-auto`.

Scroll area with a pinned footer

[Complete TSX file](/examples/scroll-area/fill.tsx)

```tsx
"use client";

import { ScrollArea } from "@/siteplane/components/scroll-area";

const projects = [
  { href: "#website-redesign", name: "Website redesign" },
  { href: "#mobile-app", name: "Mobile app" },
  { href: "#data-migration", name: "Data migration" },
  { href: "#customer-portal", name: "Customer portal" },
];

export default function Example() {
  return (
    <div className="flex h-64 w-56 flex-col overflow-hidden rounded-lg border">
      <ScrollArea fill>
        <div className="flex h-full flex-col gap-2 p-2 text-sm">
          <nav aria-label="Projects" className="flex flex-col gap-1">
            {projects.map((project) => (
              <a
                className="rounded-md px-2 py-1.5 hover:bg-accent"
                href={project.href}
                key={project.href}
              >
                {project.name}
              </a>
            ))}
          </nav>
          <footer className="mt-auto border-t px-2 pt-2 text-muted-foreground text-xs">
            4 active projects
          </footer>
        </div>
      </ScrollArea>
    </div>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| scrollFade | boolean | false | Fades each edge that has more content beyond it (top, bottom, left, right) by up to 1.5rem, growing with the distance left to scroll. An edge with nothing beyond it stays sharp. |
| scrollbar | auto \| hidden | auto | Render both automatic scrollbars or neither. |
| scrollbarGutter | boolean | false | Add viewport padding when overflow exists. |
| fill | boolean | false | Content gets full viewport size. |
| clampContentMinWidth | boolean | true | Content min-width is set to zero; false preserves intrinsic sizing. |
| aria-label / aria-labelledby | string | unset | Names the scrollable viewport, which then becomes a labeled region. |
| ScrollBar.orientation | horizontal \| vertical | vertical | Low-level scrollbar direction. |
ScrollArea wraps Base UI Root and automatically includes Viewport, Content,
vertical/horizontal ScrollBar and Corner. ScrollBar automatically includes
Thumb. `ScrollAreaPrimitive` exposes [Base UI Scroll
Area](https://base-ui.com/react/components/scroll-area). Other root props and
refs are inherited; className is a string. There is no separate viewportProps or
contentProps bag. `aria-label` and `aria-labelledby` go to the scrollable
viewport; other root props, including other ARIA attributes, apply to the root
element, which Base UI renders with `role="presentation"`. Use the lower-level
Base UI parts only if this wrapper cannot represent your use case.

[Read the installed source](/source/scroll-area).

## Usage notes

The viewport joins the tab order while its content overflows, so keyboard users can focus it and scroll with the arrow keys. Name it with `aria-label` or `aria-labelledby` so screen readers announce what it contains. `scrollbar="hidden"` hides the scrollbars but not the scrolling; keep overflow discoverable. Root scroll handlers are not a dedicated viewport callback API. Constrain the parent size; setting height only on oversized child content does not create a usable viewport.
