Skip to documentation content

Scroll Area

Keep overflowing content scrollable while applying consistent scrollbar styling.

scroll-area-demo

Overview

Use Scroll Area when content must overflow inside a bounded region — a fixed-height sidebar, an activity feed, a code pane, a wide table — and the native scrollbar's look would clash with the surrounding design. It renders a real scrolling viewport with styled, fading scrollbars while keeping native scrolling physics on every platform.

The key word is bounded: Scroll Area is for regions whose size is fixed by the layout. When the page itself can simply grow and scroll natively, prefer that — see Don't do this.

Anatomy

A scroll area has a root, a viewport that actually scrolls, optional vertical or horizontal scrollbars with thumbs, and a corner piece where both meet. The Honest UI ScrollArea wrapper adds an orientation convenience prop ("vertical", "horizontal", or "both") that mounts the matching scrollbars in one line.

Scrollbars stay invisible at rest and fade in when the region is hovered or actively scrolling, so quiet surfaces do not carry permanent chrome.

Behavior

Keyboard. The viewport is focusable: it receives a visible focus ring and arrow keys, Page Up/Page Down, and Space scroll its content once focused. That makes keyboard-only use possible without any extra wiring — but only if people can reach it, which Tab handles as long as the region sits in a sensible focus order.

Overscroll. The viewport uses overscroll-contain, so flicking to the end of an inner feed does not scroll the whole page underneath — the gesture stops at the region instead of yanking the document.

Overflow signals. The root exposes data attributes such as has-overflow-x and directional edge attributes (with a configurable overflowEdgeThreshold), which you can use for edge fades or "scroll for more" hints without measuring the DOM yourself.

Accessibility

Because the viewport takes a tab stop, a page with many nested scroll areas multiplies keyboard stops — one more reason to reserve this component for regions that truly need it. Screen readers announce nothing special about the region by default; add aria-label or tabindex-adjacent context when the bounded content is not obviously connected to a heading nearby.

Keep scrollable regions large enough to use comfortably. A region that shows two items and hides twenty invites missed content, especially on touch screens where no scrollbar is visible until interaction. If actions live inside the region — buttons, links — confirm they remain reachable by keyboard after the viewport takes its own stop; focus moves through the inner controls normally once the region itself has focus.

Scrollbar thumbs and track colors come from theme tokens (foreground/20 for the thumb), so they adapt to dark mode automatically; the bars themselves stay out of screenshots and print layouts because they are invisible until hovered or scrolled. Horizontal scrolling is appropriate for wide content like code or tables where reflowing would destroy meaning; ordinary prose should reflow rather than scroll sideways, per WCAG reflow guidance.

There is no loading or error state. Render skeletons or feedback inside the viewport while content loads.

Installation

npx honestui@latest add scroll-area

Usage

import { ScrollArea } from "@/components/ui/scroll-area";
<ScrollArea className="h-64 rounded-md border">
  <div className="p-4">
    Long content that overflows vertically…
  </div>
</ScrollArea>

The height (or max-height) comes from you — pass orientation="both" when content can overflow in both directions, and keep padding inside an inner wrapper if you want it to scroll with the content.

Don't do this

Wrapping primary page content

// Bad
<ScrollArea className="h-screen">
  <main>{/* The entire page */}</main>
</ScrollArea>
// Good
<main>{/* Normal document flow */}</main>

Nested scroll traps fight the browser: the wheel scrolls the wrong container first, browser find-in-page and anchor jumps misbehave, momentum feels foreign, and mobile browsers show their own overlays anyway. Native document scrolling preserves zoom behavior and URL fragments for free. Reserve Scroll Area for regions the design genuinely pins in place.

Hiding required actions in a tiny region

// Bad
<ScrollArea className="h-16 rounded-md border">
  {terms}
  {/* Agree button lives below the fold inside */}
</ScrollArea>
// Good
<ScrollArea className="h-40 rounded-md border">{terms}</ScrollArea>
<div className="mt-3">
  <Button>I agree to the terms above</Button>
</div>

A region too short to reveal its own contents hides decisions from people who never discover it scrolls — a real accessibility failure for anyone who does not see the faint custom thumb. Keep required content and actions outside the scrollable area, or make the region tall enough to surface what matters.

Horizontal scrolling for plain text

// Bad
<ScrollArea orientation="horizontal" className="w-full">
  <p className="whitespace-nowrap">{sentence}</p>
</ScrollArea>
// Good
<p>{sentence}</p>

Prose should wrap; forcing it sideways breaks reading flow, hides most of the sentence at any moment, and fails WCAG reflow expectations. Horizontal scroll areas belong to inherently wide content: code blocks, data tables, timelines, kanban lanes.

Examples

Activity Feed

A fixed-height panel where new events arrive below the fold — the canonical bounded-feed case.

scroll-area-activity

Horizontal Scroll

Wide content scrolls sideways under a slim horizontal bar.

scroll-area-horizontal

Both Scrollbars

Content overflowing in both directions, with the corner piece closing the gap between bars.

scroll-area-both

API reference

ScrollArea forwards Base UI root props (including overflowEdgeThreshold) and adds:

PropValuesDefault
orientation"vertical", "horizontal", "both""vertical"

The wrapper renders the viewport (which receives the focus ring and overscroll-contain), mounts the matching ScrollBar parts from orientation, and always renders the corner. ScrollBar accepts Base UI scrollbar props including orientation; thumbs fade in via data-hovering and data-scrolling states. Root state attributes such as has-overflow-x and per-edge overflow flags are available for building edge fades or hints.

See the Base UI Scroll Area API.