Scroll Area

A scroll area is a fixed-size region whose content scrolls inside it. Its scrollbars follow the theme instead of changing with each browser and system.

import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";

const tags = Array.from({ length: 30 }, (_, index) => `v1.${30 - index}.0`);
const tiles = Array.from({ length: 10 }, (_, index) => index + 1);

export default function ScrollAreaDemo() {
  return (
    <>
      <ScrollArea className="h-56 w-48">
        <div className="p-4">
          <h4 className="mb-3 text-sm font-medium text-text-secondary">Releases</h4>
          {tags.map((tag, index) => (
            <div key={tag}>
              <p className="py-2 font-mono text-sm">{tag}</p>
              {index < tags.length - 1 && <Separator />}
            </div>
          ))}
        </div>
      </ScrollArea>
      <ScrollArea className="w-72 max-w-full">
        <div className="flex w-max gap-3 p-4">
          {tiles.map((tile) => (
            <div
              key={tile}
              className="flex size-24 shrink-0 items-center justify-center rounded-md border border-border bg-surface font-mono text-sm text-text-secondary"
            >
              {tile}
            </div>
          ))}
        </div>
      </ScrollArea>
    </>
  );
}

The code

The component is a single file, read from the repository when the site is built. Press Show code to see all of it.

import type { ComponentProps } from "react";
import { ScrollArea as BaseScrollArea } from "@base-ui/react/scroll-area";
import { cx } from "@/lib/cx";
import type { WithClass } from "@/lib/types";

// Scrollbars that stay out of the way: they are invisible until you hover the area or scroll,
// and only appear in a direction that actually overflows. Give the area a size with classes
// (h-48 w-64), and put your content inside. The viewport can be focused, so the keyboard can
// scroll it with the arrow keys.
const viewportRing =
  "outline-hidden focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-text";

function Scrollbar({ orientation }: { orientation: "vertical" | "horizontal" }) {
  return (
    <BaseScrollArea.Scrollbar
      orientation={orientation}
      className="flex touch-none p-0.5 opacity-0 transition-opacity duration-150 select-none data-[hovering]:opacity-100 data-[scrolling]:opacity-100 data-[orientation=horizontal]:h-3 data-[orientation=horizontal]:flex-col data-[orientation=vertical]:w-3 motion-reduce:transition-none"
    >
      <BaseScrollArea.Thumb className="relative flex-1 rounded-full bg-text-secondary" />
    </BaseScrollArea.Scrollbar>
  );
}

export function ScrollArea({
  className,
  children,
  ...props
}: WithClass<Omit<ComponentProps<typeof BaseScrollArea.Root>, "children">> & { children?: React.ReactNode }) {
  return (
    <BaseScrollArea.Root className={cx("relative overflow-hidden rounded-lg border border-border", className)} {...props}>
      <BaseScrollArea.Viewport className={cx("size-full rounded-[inherit]", viewportRing)}>
        <BaseScrollArea.Content>{children}</BaseScrollArea.Content>
      </BaseScrollArea.Viewport>
      <Scrollbar orientation="vertical" />
      <Scrollbar orientation="horizontal" />
      <BaseScrollArea.Corner />
    </BaseScrollArea.Root>
  );
}

Props

PropTypeDefaultWhat it does
classNamestring—Give the area a size with it, such as h-56 w-48. Without a size it grows to fit its content and nothing scrolls.
childrenReactNode—The content that scrolls. It can be taller, wider or both.

Every other prop goes to the Base UI ScrollArea underneath.

The thinking

Scrolling inside a box is sometimes the right call: a long list in a side panel, a row of cards, a code listing. The box needs to say that it scrolls.

  • Built on Base UI: Base UI draws its own scrollbars over a native scrolling viewport. The scrolling itself is the browser's, so wheel, touch, momentum and keyboard all behave as people expect.
  • Both directions: A vertical and a horizontal scrollbar are built in. Each one only appears when the content overflows in that direction.
  • Same look everywhere: Native scrollbars differ between Windows, macOS and each browser. These are the same thick and the same color everywhere.

Interface design

  • Border: A hairline border and the lg radius, so the edge of the area is clear.
  • Quiet scrollbars: They are invisible until you hover the area or scroll it, then fade in. The thumb is a rounded bar in the secondary text color.
  • Padding: The area has none. Put padding on the content inside, so the scrollbar can sit at the edge.

Interaction

  • Scroll: Use the mouse wheel, a trackpad, touch, or drag the thumb.
  • Keyboard: The area takes focus with Tab and shows a ring. The arrow keys, Page Up, Page Down, Home and End then scroll it.
  • Motion: The scrollbars fade in over 150ms. With reduced motion turned on they appear at once.

Empathy

  • Reachable by keyboard: Without focus a keyboard user can't scroll a box. That is why the area itself is a tab stop.
  • Show that there is more: Because scrollbars are hidden at rest, let content run past the edge, or cut it off mid-row, so people see there is more to scroll to.
  • Use sparingly: A scroll inside a page that also scrolls can trap a wheel or a thumb. Prefer the page for long content.