Pho Design System

Scroll Area

A capped scroll region with overlay scrollbars and progressive edge fade. Built on Base UI ScrollArea — native scroll still does the work, so keyboard scrolling and momentum are unchanged. Approaching rows blur (0 → 1px) and wash into the surface; strength tracks how far content has scrolled under the edge.

Import

import { ScrollArea } from "@photon-ai/pho-ui/components/scroll-area";

Basic

Give ScrollArea.Root a fixed size, put the content in Viewport → Content, and add a Scrollbar → Thumb. The bar sits over the content and fades in on hover or while scrolling, so it never shifts layout. Edges blur and wash in as you scroll.

<ScrollArea.Root className="h-44 w-72">
  <ScrollArea.Viewport>
    <ScrollArea.Content className="p-3">…</ScrollArea.Content>
  </ScrollArea.Viewport>
  <ScrollArea.Scrollbar>
    <ScrollArea.Thumb />
  </ScrollArea.Scrollbar>
</ScrollArea.Root>

Both axes

Add a Scrollbar per axis — the default orientation is vertical; set orientation="horizontal" for the second. A Corner fills the square where the two bars meet.

<ScrollArea.Root className="h-48 w-80">
  <ScrollArea.Viewport>
    <ScrollArea.Content className="p-3">…</ScrollArea.Content>
  </ScrollArea.Viewport>
  <ScrollArea.Scrollbar orientation="vertical">
    <ScrollArea.Thumb />
  </ScrollArea.Scrollbar>
  <ScrollArea.Scrollbar orientation="horizontal">
    <ScrollArea.Thumb />
  </ScrollArea.Scrollbar>
  <ScrollArea.Corner />
</ScrollArea.Root>

Resting inset

inset on Content is a resting inset in the scroll axis — a scroll view’s safe-area padding. The content stands clear of the ends at rest but scrolls past them to the true edge, where the edge fade and the scrollbar stay. insetTop / insetBottom set the ends separately and win over inset. This is not padding on Root: that would clip the content at the inset instead of letting it pass under.

<ScrollArea.Root className="h-44 w-72 rounded-base border border-pho-secondary">
  <ScrollArea.Viewport>
    <ScrollArea.Content inset={20} className="px-3">
      {releases.map(…)}
    </ScrollArea.Content>
  </ScrollArea.Viewport>
  <ScrollArea.Scrollbar>
    <ScrollArea.Thumb />
  </ScrollArea.Scrollbar>
</ScrollArea.Root>

Anatomy

Props

Every part renders a <div> and forwards className plus native <div> attributes. The parts that add their own props:

Root exposes overflow state as data-* attributes (data-has-overflow-x, data-overflow-y-end, …) and the active scrollbar carries data-hovering, data-scrolling, and data-orientation for styling.

Accessibility

Keyboard: when the content overflows, ScrollArea.Viewport is placed in the tab order (tabIndex={0}) so a keyboard user can focus the region and scroll it; a non-scrollable viewport is removed from the tab order (tabIndex={-1}). Once focused it scrolls with the platform’s native keys:

Best practices