ScrollArea
A bounded scrolling region with a thin neutral scrollbar that keeps a card or panel's overflow reachable without replacing the page's own native scroll.
Preview
scroll-area · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/scroll-area
The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.
Usage
import { ScrollArea } from "@/components/ui/scroll-area"<ScrollArea maxHeight="16rem" className="rounded-lg border">
<ul>
{items.map((item) => (
<li key={item.id}>{item.label}</li>
))}
</ul>
</ScrollArea>When to use it
Use it when
- Bounding one card, panel or list to a height where its content is longer than the box.
- A wide row of cards or a table that scrolls sideways inside its column.
- Holding one block's height steady on a surface that still scrolls natively.
Do not use it when
- Wrapping the whole page or the reading column, where a bounded height clips content at 200% text.Use the browser's own page scroll instead.
- A small block that already fits, wrapped on the chance that it might grow.Use
cardinstead.
Anatomy
ScrollAreaThe root. Takes the region's width, border and rounding, and draws neutral chrome on neither colour axis.Controlled byclassNameScrollArea.ViewportThe scrollable element. Base UI gives it overflow scroll and a tab stop. maxHeight and the focus ring land here.Controlled bymaxHeightScrollArea.ScrollbarThe rail for one axis. Its track is transparent, and it fades in on hover and while scrolling.Controlled byorientationScrollArea.ThumbThe grabbable handle. Base UI sizes its length, and this component paints it the muted-foreground role.ScrollArea.CornerThe square where two rails meet, drawn only when orientation is both. It draws no fill.
- ScrollArea 1The root; neither data-status nor data-category
- ScrollArea.Viewport 1One tab stop, carries the focus ring
- ScrollArea.Scrollbar 1..2One per scrolling axis; transparent track
- ScrollArea.Thumb 1The muted-foreground handle
- ScrollArea.Corner 0..1Drawn only when orientation is both
Examples
A long list
The list outruns its box, so maxHeight bounds the region and the rail appears
while the page around it keeps its native scroll.
scroll-area-a-long-list · base base · style base-lyraOpen under the product themeIn a card
One bounded panel with a heading fixed above the region, so maxHeight makes
only the card body overflow and scroll.
scroll-area-in-a-card · base base · style base-lyraOpen under the product themeAccessibility
This component was audited against WCAG 2.2 AA in a source pass and a rendered pass. The audit is author-run, not an independent review, and clinical review is still pending.
The audit found one serious defect and fixed it. The viewport was not a reliable
keyboard tab stop and carried no accessible name, so a screen reader that reached
it met an unnamed stop. It now holds a tab stop and takes a name through label
or labelledBy, and the default demo and both examples name their region, one by
pointing labelledBy at its on-screen heading.
The rendered pass found nothing else to change: no colour-axis, token, px, or
colour-literal issue, and reflow and target size passed. Two items stay open. In
the card example the rounded corners can partly clip the focus ring at the flush
bottom edge, though the indicator stays perceivable. The thumb's contrast and the
pairs below are not measured in source, so the report below stands as its own
record.
| Key | Action | Notes |
|---|---|---|
| Tab | Moves focus onto the viewport when it is scrollable | One tab stop for the region. Base UI drops it when there is nothing to scroll. |
| Shift+Tab | Moves focus back out of the viewport | A single stop in reverse too, so focus does not step through the rails. |
| Arrow keys | Scroll the focused viewport | The browser's own behaviour on a focused scroll container, along the allowed axes. |
| Page Up, Page Down | Scroll the focused viewport by a page | The browser's default, not a key this component binds. |
| Home, End | Scroll to the start or end of the focused viewport | Again the browser's default on a scroll container. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
Data attributes
| Attribute | Condition | Value |
|---|---|---|
| data-slot | On the root and every inner part, always | scroll-area, scroll-area-viewport, scroll-area-scrollbar, scroll-area-thumb, scroll-area-corner |
| data-orientation | On a scrollbar, set by Base UI | vertical or horizontal |
| data-hovering, data-scrolling | On a scrollbar while hovered or while the region scrolls | What the rail's fade-in is keyed to |
API reference
Prop
Type
Generated from ScrollAreaProps in registry/bases/base/scroll-area.tsx.
maxHeight gives the region something to overflow, so set it in a rem rather
than a pixel count. Given neither it nor a height class, the region grows to fit
and a development warning names the omission. className is the one route by
which colour reaches the region, and the two-colour-axes rule applies in full.
Related
Divider
A thin line that separates two groups of content, with an optional centred label, drawn as a neutral hairline and carrying no status or category colour.
Avatar
A picture or initials standing for a person, with the person's name as its accessible label and a neutral fallback when there is no picture.