Scroll Area
A cross-browser scroll container on radix-ui with a thin token-colored scrollbar - vertical, horizontal or both axes.
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
const tags = Array.from({ length: 24 }, (_, i) => `Component v1.${i}.0`)
export default function ScrollAreaDemo() {
return (
<ScrollArea className="h-56 w-64 rounded-lg border border-border">
<div className="p-4">
<p className="text-sm font-medium">Changelog</p>
<Separator className="my-2" />
{tags.map((tag) => (
<div key={tag} className="py-1.5 text-sm text-muted-foreground">
{tag}
</div>
))}
</div>
</ScrollArea>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/scroll-areaDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install radix-ui@^1.6.1Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/scroll-area.tsx"use client"
import type * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "@/components/ui/primitives"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="size-full rounded-[inherit] outline-none transition-[color,box-shadow] duration-(--motion-fast) focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none select-none p-px transition-colors duration-(--motion-fast)",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }Manual installs skip the @ai2/tokens theme - add the token CSS from the theming guide or the scrollbar color will be missing.
Usage
import { ScrollArea } from "@/components/ui/scroll-area"
<ScrollArea className="h-56 w-64 rounded-lg border border-border">
<div className="p-4">
{items.map((item) => (
<div key={item} className="py-1.5 text-sm">
{item}
</div>
))}
</div>
</ScrollArea>Set a fixed height or max-height on the ScrollArea so its content can overflow. The vertical scrollbar is rendered for you.
Examples
Vertical
The default bar is vertical, so a tall list needs only the ScrollArea wrapper with a height.
Horizontal
For the x axis, let the inner track grow with w-max and add a ScrollBar orientation="horizontal" inside the ScrollArea.
Both axes
When content overflows on both axes, the vertical and horizontal bars appear together and radix adds a corner between them.
Props
ScrollArea
Forwards the radix ScrollArea.Root props. The most useful ones are listed below.
| Prop | Type | Default | Description |
|---|---|---|---|
type | "auto" | "always" | "scroll" | "hover" | "hover" | When the scrollbar is shown, forwarded to the radix ScrollArea root. hover reveals it on pointer over, always keeps it visible. |
scrollHideDelay | number | 600 | Milliseconds to wait before hiding the scrollbar after the pointer leaves, when type is hover or scroll. |
className | string | - | Merged onto the root. Set a fixed height or max-height here so the content can overflow and scroll. |
ScrollBar
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "vertical" | "horizontal" | "vertical" | Axis the scrollbar controls. ScrollArea renders a vertical bar automatically; add a horizontal one inside for the x axis. |
className | string | - | Merged onto the scrollbar track for custom sizing or spacing. |
ai2 Scroll Area: a themed scroll container for React
The ai2 Scroll Area is a shadcn-compatible scroll container for React, built on the radix-ui ScrollArea primitive and styled with Tailwind CSS v4. It replaces the native browser scrollbar with a thin, token-colored bar that looks the same across browsers and platforms, on both the vertical and horizontal axes.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Scroll Area?
It is a two-part composition: ScrollArea wraps a radix root and viewport around your content, and ScrollBar renders the draggable track. The root already includes a vertical ScrollBar, so most lists need only the single ScrollArea wrapper. The anatomy matches shadcn/ui, so existing snippets and AI agents keep working without changes.
Give the ScrollArea a fixed height or max-height and its content overflows into a scrollable viewport. For horizontal scrolling, add a ScrollBar orientation="horizontal" inside and let the inner content grow wider than the container. The thumb uses the border token, so the bar stays subtle and readable in both light and dark mode.
Why use it
- Consistent across browsers: The radix-ui ScrollArea primitive hides the native scrollbar and renders its own, so the bar looks identical on Chrome, Safari, Firefox and every OS.
- Token-colored, never loud: The thumb uses the border token and the track is only 2.5 units wide, so the scrollbar reads as a hint rather than chrome, in both themes.
- Vertical and horizontal: The root ships a vertical bar; drop in a second ScrollBar with orientation horizontal for the x axis, or both for two-axis scrolling with an auto corner.
- Keyboard and focus ready: The viewport is focusable with a focus-visible ring, so keyboard users can scroll the region with arrow keys once it is focused.
- Agent-readable metadata: The registry item describes its parts and the fixed-height requirement in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component and the @ai2/tokens theme to your project, with radix-ui as the only runtime dependency.
- Two-part anatomy: ScrollArea and ScrollBar mirror shadcn/ui exactly, so copy-paste snippets and agent output drop in unchanged.
- Auto vertical scrollbar: ScrollArea renders a vertical ScrollBar and a corner element for you, so a simple list needs only the wrapper and a height.
- Horizontal orientation: ScrollBar accepts orientation="horizontal" for wide content like card rows, code blocks or tables, alongside the default vertical bar.
- Themed thumb via tokens: The thumb is colored from the border token rather than a hardcoded value, so it tracks your theme automatically in light and dark mode.
- Data attributes for styling: Every part exposes data-slot (scroll-area, scroll-area-viewport, scroll-area-scrollbar, scroll-area-thumb) so you can restyle from CSS without forking.
Production tips
- Always set a height: A ScrollArea only scrolls when its content is taller (or wider) than the box. Put a fixed height or max-height on the ScrollArea via className, not on the inner content.
- Use w-max for horizontal rows: For a horizontal scroller, let the inner track grow past the container with a w-max or fixed-width child, and add ScrollBar orientation horizontal so the bar appears.
- Keep whitespace-nowrap on rows: When scrolling a row of items horizontally, add whitespace-nowrap so the items stay on one line instead of wrapping and collapsing the overflow.
- Do not nest scroll areas needlessly: A page rarely needs a scroll area inside another. Reserve the component for bounded regions like sidebars, changelogs, command lists or card carousels.
- Round the corners on both parts: The viewport inherits the root radius, so a rounded-lg border on the ScrollArea clips the content cleanly without extra classes on the inner element.
Works with the rest of ai2
Scroll Area bounds long content across the registry. Wrap a ai2 Command list or the items inside a ai2 Select so a long menu scrolls instead of running off screen, and pair it with a ai2 Separator to group rows.
Put one inside a ai2 Dialog or ai2 Sheet to keep a tall body inside the viewport while the header and footer stay fixed. Everything shares one token source, so the scrollbar matches the surrounding surface in both modes.