Scroll Area
A scrollable container with custom overlay scrollbars that do not steal layout space. Supports vertical, horizontal, and both-axis content with a neutral branded thumb, RTL, and a compound API for full control.
Playground
Installation
pnpm add @tessinaui/uiUsage
import {
ScrollArea,
ScrollAreaViewport,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaCorner,
ScrollAreaContent,
} from "@tessinaui/ui";<ScrollArea className="h-72 w-64 rounded-md border border-border bg-card">
<div className="p-4">{/* tall content */}</div>
</ScrollArea>Showcase
When to use ScrollArea vs. native overflow
| Use ScrollArea when… | Use native overflow-auto when… |
|---|---|
| You want a thin, branded scrollbar that doesn't push content | The OS scrollbar is fine |
| The container has a fixed height and tall content | Scroll is at the page level |
| You need a consistent rounded thumb with size + visibility controls | You don't need custom visuals |
| You want horizontal scrolling with a pretty rail | The default rail is good enough |
ScrollArea renders its scrollbars as overlays — they appear on top of the content, so the viewport width/height doesn't shift when they show/hide.
Anatomy
Most use-cases only need <ScrollArea>. When orientation is set, the appropriate scrollbar(s) and corner are auto-rendered.
<ScrollArea orientation="both">
{/* content that overflows both axes */}
</ScrollArea>For custom layouts, pass asChildParts and compose the primitives yourself:
<ScrollArea asChildParts>
<ScrollAreaViewport>{/* content */}</ScrollAreaViewport>
<ScrollAreaScrollbar orientation="vertical">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
<ScrollAreaScrollbar orientation="horizontal">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
<ScrollAreaCorner />
</ScrollArea>Size
| Size | Track thickness | Thumb |
|---|---|---|
"sm" | 6px | Thin — good for dense UIs, chat transcripts |
"md" (default) | 10px | Default — lists, panels |
"lg" | 14px | Prominent — long-form reading, data tables |
Rounded
Applies to the thumb (the track is transparent).
| Value | CSS |
|---|---|
"none" | rounded-none |
"sm" | rounded-sm |
"md" | rounded-md |
"full" (default) | rounded-full |
Thumb colour
The thumb is a single neutral palette in both light and dark modes — built from the muted-foreground token. It composites at 50% opacity at rest, bumps to 70% on hover, and goes fully opaque while actively dragged.
Rationale: a scrollbar is chrome, not content. We deliberately don't ship semantic (error/warning/success/info) colouring for the thumb — use the surrounding surface, alert, or status components to signal intent instead.
Visibility
Controls when the scrollbar is rendered.
| Value | Behaviour |
|---|---|
"always" (default) | Visible whenever the viewport overflows. Good for lists, logs, and most panels where users need the affordance. |
"hover" | Visible on hover or while scrolling; hidden otherwise. Use for clean canvases where the thumb would distract. |
"scroll" | Visible only while scrolling. |
<ScrollArea visibility="hover">…</ScrollArea>Rail
Controls whether the scrollbar has a visible track background under the thumb.
| Value | Behaviour |
|---|---|
false (default) | Only the thumb renders; it sits as an overlay over the content. Minimal, calm, and matches most product surfaces. |
true | A subtle muted background paints under the thumb to form a visible track — closer to a native OS scrollbar. Use when the thumb alone feels floating or when users benefit from seeing the full scrollable extent. |
{/* Overlay — default */}
<ScrollArea>…</ScrollArea>
{/* With a visible rail under the thumb */}
<ScrollArea rail>…</ScrollArea>Orientation
orientation controls which scrollbars render. The default is vertical-only.
{/* Row of cards that scrolls horizontally */}
<ScrollArea orientation="horizontal">
<div className="flex gap-4 p-4 w-max">…</div>
</ScrollArea>
{/* Data table that scrolls in both dimensions */}
<ScrollArea orientation="both">
<table className="w-max">…</table>
</ScrollArea>When orientation="both", a transparent corner is rendered at the scrollbar intersection so the two rails don't overlap.
RTL
Pass dir="rtl" to the root. The vertical scrollbar flips to the logical end (visual left in RTL), and horizontal scrolling reverses direction.
<ScrollArea dir="rtl">
<div className="text-right">…</div>
</ScrollArea>Compound API
For advanced layouts — custom viewport wrappers, multiple content blocks, extra styling on specific parts — pass asChildParts and compose directly:
<ScrollArea asChildParts className="h-64 rounded-md border">
<ScrollAreaViewport className="p-4">
<h4>Section A</h4>
<ul>…</ul>
<h4>Section B</h4>
<ul>…</ul>
</ScrollAreaViewport>
<ScrollAreaScrollbar orientation="vertical">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>ScrollAreaContent is also available if you want an inner sizing wrapper that still inherits the root's corner radius.
Accessibility
- The viewport is the focusable element — it automatically receives
tabindex={0}via Base UI when scrollable, so keyboard users can page through content withArrowUp/ArrowDown/PageUp/PageDown. - The custom scrollbar is purely visual; keyboard scrolling is delegated to the browser's native behaviour on the viewport.
data-[hovering],data-[scrolling], anddata-[orientation]attributes are surfaced by Base UI for CSS theming.- Because the scrollbars are overlays, they do not steal layout space — which means text reflow is consistent across devices where native scrollbars differ.
API Reference
ScrollArea (root)
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "lg" | "md" | Scrollbar thickness |
rounded | "none" | "sm" | "md" | "full" | "full" | Thumb corner radius |
visibility | "always" | "hover" | "scroll" | "always" | When the scrollbar is visible |
rail | boolean | false | Paint a subtle muted background under the scrollbar to form a visible track/rail. When false, only the thumb is rendered as an overlay. |
orientation | "vertical" | "horizontal" | "both" | "vertical" | Which scrollbars to render |
dir | "ltr" | "rtl" | "ltr" | Reading direction |
asChildParts | boolean | false | Opt out of auto-rendering; compose parts manually |
overflowEdgeThreshold | number | { xStart, xEnd, yStart, yEnd } | 0 | Pixel threshold before overflow edge attributes apply |
All other standard <div> props (including className, style, id, role, data-attributes) are forwarded to the root element.
ScrollAreaViewport
The scrollable container. Renders a <div>; props extend the Base UI primitive. Use className to add padding or constrain the inner content.
ScrollAreaScrollbar
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "vertical" | "horizontal" | "vertical" | Which axis this scrollbar controls |
keepMounted | boolean | false | Keep in DOM even when no overflow |
size, visibility, rail | — | inherited | Override the root's variant |
ScrollAreaThumb
| Prop | Type | Default | Description |
|---|---|---|---|
rounded | — | inherited | Override the root's variant |
ScrollAreaCorner
Renders a <div> at the intersection of the horizontal and vertical scrollbars. Defaults to transparent.
ScrollAreaContent
Optional inner wrapper inside the viewport. Inherits border-radius via rounded-[inherit]. Useful when you want Base UI's content-size awareness for scrollbar math.
Notes
- Built on
@base-ui/react/scroll-area— the same headless primitive the design system uses for custom scroll affordances. - The scrollbar uses Base UI's
data-[hovering]anddata-[scrolling]attributes for visibility transitions, so styles automatically respond to mouse and scroll activity. - Overlay behaviour means the scrollbar never changes the viewport's width — layout stays stable on first paint and when the bar appears/disappears.
- For platforms where native scrollbars are always visible (Windows classic), this component gives you a consistent cross-platform look.
Divider
A thin line that separates content. Horizontal or vertical, solid / dashed / dotted, five thickness sizes, tonal colours, optional inline label, and RTL support.
Table
A complete primitive set for tabular data — header, body, footer, sortable columns, sticky header, row states, plus cell helpers for avatars, status, currency amounts, and actions. Designed in the Wise / PrimeReact tradition.