Scroll Area
A scrollable region with custom-styled scrollbars that match the design system. Content must be explicitly sized.
Vertical
Horizontal
Installation
pnpm dlx @hex-core/cli add scroll-areaHorizontal scroll thumbnails
Render a horizontal flex row inside ScrollArea. The wrapper auto-mounts both vertical and horizontal scrollbars; horizontal overflow comes from `whitespace-nowrap` plus the inner `flex w-max` row.
<ScrollArea className="w-full max-w-md whitespace-nowrap rounded-md border">
<div className="flex w-max gap-3 p-3">
{photos.map((photo) => (
<figure key={photo.id} className="shrink-0">
<img
src={photo.thumb}
alt={photo.alt}
className="h-28 w-40 rounded-md object-cover"
/>
<figcaption className="pt-2 text-xs text-muted-foreground">{photo.caption}</figcaption>
</figure>
))}
</div>
</ScrollArea>Auto-scroll to bottom (chat messages)
Pin the viewport to the latest message. Use a ref on the ScrollArea root, then scroll the inner Radix viewport (selected via [data-radix-scroll-area-viewport]) on every messages change.
import { useEffect, useRef } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
interface Message {
id: string;
author: string;
body: string;
}
export function ChatLog({ messages }: { messages: Message[] }) {
const rootRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const viewport = rootRef.current?.querySelector<HTMLDivElement>(
"[data-radix-scroll-area-viewport]",
);
if (!viewport) return;
viewport.scrollTop = viewport.scrollHeight;
}, [messages]);
return (
<ScrollArea ref={rootRef} className="h-72 w-full rounded-md border">
<ul className="space-y-2 p-3">
{messages.map((m) => (
<li key={m.id} className="text-sm">
<span className="font-medium">{m.author}:</span> {m.body}
</li>
))}
</ul>
</ScrollArea>
);
}API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
type | "auto" | "always" | "scroll" | "hover" | hover | When scrollbars are visible |
className | string | — | Set dimensions via Tailwind (e.g. h-72 w-48) |
viewportTabIndex | number | 0 | tabIndex applied to the scroll viewport. Defaults to 0 so keyboard users can scroll without a pointer; pass -1 to skip the viewport in the tab order when wrapping decorative or already-keyboard-reachable content. |
AI Guidance
When to use
Use when you need styled scrollbars that match the design system — sidebars, code blocks, large lists in dialogs. Must have explicit dimensions (height/width).
When not to use
Don't use for the whole page (use native browser scrollbars). Don't use for content that should grow freely (omit the ScrollArea and use overflow-auto directly).
Common mistakes
- Forgetting to set height/width — scrollbars don't appear
- Using for the whole page
- Nesting ScrollAreas (confusing UX)
- Wrapping decorative or already-keyboard-reachable content without setting viewportTabIndex={-1} — adds an unnecessary tab stop
Accessibility
The viewport is keyboard-focusable by default (viewportTabIndex=0) so users can scroll long content via arrow keys / PgUp / PgDn / Home / End without a pointer. Pass viewportTabIndex={-1} when the contents are already in the tab order or purely decorative. For very long lists, consider pagination or virtualization.
Token budget: 350
Verified against @hex-core/components@1.12.0