# Scroll Area

> Keep overflowing content scrollable while applying consistent scrollbar styling.

Source: https://www.honestui.com/docs/components/scroll-area

```tsx
import { ScrollArea } from "@/components/honest-ui/ui/scroll-area"

const tags = Array.from({ length: 50 }, (_, i) => `v1.0.0-alpha.${i}`)

export function ScrollAreaDemo() {
  return (
    <ScrollArea className="w-full max-w-64 h-64 rounded-md border">
      <div className="px-4 py-2">
        <h4 className="mb-2 text-sm font-medium">Tags</h4>
        <div className="flex flex-col gap-1">
          {tags.map((tag) => (
            <div key={tag} className="text-sm">
              {tag}
            </div>
          ))}
        </div>
      </div>
    </ScrollArea>
  )
}

```

## Overview [#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](#dont-do-this).

## Anatomy [#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 [#behavior]

**Keyboard.** The viewport is focusable: it receives a visible focus ring and arrow keys, <kbd>Page Up</kbd>/<kbd>Page Down</kbd>, and <kbd>Space</kbd> scroll its content once focused. That makes keyboard-only use possible without any extra wiring — but only if people can reach it, which <kbd>Tab</kbd> 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 [#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 [#installation]


  

  
    <CliBlock commands="[&#x22;scroll-area&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/scroll-area.tsx

```tsx
"use client"

import { ScrollArea as ScrollAreaPrimitive } from "@base-ui-components/react/scroll-area"

import { cn } from "@/lib/utils"

function ScrollArea({
  className,
  children,
  orientation,
  ...props
}: ScrollAreaPrimitive.Root.Props & {
  orientation?: "horizontal" | "vertical" | "both"
}) {
  return (
    <ScrollAreaPrimitive.Root className="min-h-0" {...props}>
      <ScrollAreaPrimitive.Viewport
        data-slot="scroll-area-viewport"
        className={cn(
          "size-full overscroll-contain rounded-[inherit] transition-[box-shadow] outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
          className
        )}
      >
        {children}
      </ScrollAreaPrimitive.Viewport>
      {orientation === "both" ? (
        <>
          <ScrollBar orientation="vertical" />
          <ScrollBar orientation="horizontal" />
        </>
      ) : (
        <ScrollBar orientation={orientation} />
      )}
      <ScrollAreaPrimitive.Corner data-slot="scroll-area-corner" />
    </ScrollAreaPrimitive.Root>
  )
}

function ScrollBar({
  className,
  orientation = "vertical",
  ...props
}: ScrollAreaPrimitive.Scrollbar.Props) {
  return (
    <ScrollAreaPrimitive.Scrollbar
      data-slot="scroll-area-scrollbar"
      orientation={orientation}
      className={cn(
        "m-0.5 flex opacity-0 transition-opacity delay-300 data-hovering:opacity-100 data-hovering:delay-0 data-hovering:duration-100 data-scrolling:opacity-100 data-scrolling:delay-0 data-scrolling:duration-100 data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:flex-col data-[orientation=vertical]:w-1.5",
        className
      )}
      {...props}
    >
      <ScrollAreaPrimitive.Thumb
        data-slot="scroll-area-thumb"
        className="relative flex-1 rounded-full bg-foreground/20"
      />
    </ScrollAreaPrimitive.Scrollbar>
  )
}

export { ScrollArea, ScrollBar }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

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

```tsx
<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 [#dont-do-this]

### Wrapping primary page content [#wrapping-primary-page-content]

```tsx
// Bad
<ScrollArea className="h-screen">
  <main>{/* The entire page */}</main>
</ScrollArea>
```

```tsx
// 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 [#hiding-required-actions-in-a-tiny-region]

```tsx
// Bad
<ScrollArea className="h-16 rounded-md border">
  {terms}
  {/* Agree button lives below the fold inside */}
</ScrollArea>
```

```tsx
// 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 [#horizontal-scrolling-for-plain-text]

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

```tsx
// 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 [#examples]

### Activity Feed [#activity-feed]

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

```tsx
import { ScrollArea } from "@/components/honest-ui/ui/scroll-area"

const events = ["Build passed", "Comment added", "Deploy started", "Review requested", "Issue linked", "Branch merged"]

export function ScrollAreaActivity() {
  return (
    <ScrollArea className="h-40 w-72 rounded-xl border p-3">
      <div className="grid gap-2">
        {events.map((event) => <div key={event} className="rounded-lg bg-muted px-3 py-2 text-sm">{event}</div>)}
      </div>
    </ScrollArea>
  )
}

```

### Horizontal Scroll [#horizontal-scroll]

Wide content scrolls sideways under a slim horizontal bar.

```tsx
import { ScrollArea } from "@/components/honest-ui/ui/scroll-area"

export function ScrollAreaHorizontal() {
  return (
    <ScrollArea className="max-w-96 rounded-md border" orientation="horizontal">
      <div className="flex w-max gap-4 p-4">
        {Array.from({ length: 20 }).map((_, i) => (
          <div
            key={i}
            className="flex h-20 w-32 shrink-0 items-center justify-center rounded-md bg-muted"
          >
            <span className="text-sm font-medium">Item {i + 1}</span>
          </div>
        ))}
      </div>
    </ScrollArea>
  )
}

```

### Both Scrollbars [#both-scrollbars]

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

```tsx
import { ScrollArea } from "@/components/honest-ui/ui/scroll-area"

export function ScrollAreaBoth() {
  return (
    <ScrollArea orientation="both" className="h-80 max-w-80 rounded-md border">
      <p className="min-w-100 p-4">
        Just as suddenly as it had begun, the sensation stopped, leaving
        Alice feeling slightly disoriented. She looked around and realized that
        the room hadn’t changed at all—it was she who had grown smaller,
        shrinking down to a fraction of her previous size. Alice felt herself
        growing larger and larger, filling up the entire room until she feared
        she might burst. The sensation was both thrilling and terrifying, as if
        she were expanding beyond the confines of her own body. She wondered if
        this was what it felt like to be a balloon, swelling with air until it
        could hold no more. Alice peered into the mirror, her reflection staring
        back at her with an air of mischief. She wondered what it would be like
        to step through the glass and into the world beyond, where everything
        seemed to be topsy-turvy and nothing was quite as it seemed. It’s no use
        going back to yesterday, because I was a different person then,
        reflected Alice.
      </p>
    </ScrollArea>
  )
}

```

## API reference [#api-reference]

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

| Prop          | Values                                 | Default      |
| ------------- | -------------------------------------- | ------------ |
| `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](https://base-ui.com/react/components/scroll-area#api-reference).
