opsinjs

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

Theme
Density
Text
Status
scroll-area · base base · style base-lyraOpen under the product theme

Installation

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 card instead.

Anatomy

  1. ScrollAreaThe root. Takes the region's width, border and rounding, and draws neutral chrome on neither colour axis.Controlled by className
  2. ScrollArea.ViewportThe scrollable element. Base UI gives it overflow scroll and a tab stop. maxHeight and the focus ring land here.Controlled by maxHeight
  3. ScrollArea.ScrollbarThe rail for one axis. Its track is transparent, and it fades in on hover and while scrolling.Controlled by orientation
  4. ScrollArea.ThumbThe grabbable handle. Base UI sizes its length, and this component paints it the muted-foreground role.
  5. 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.

Theme
Density
Text
Status
scroll-area-a-long-list · base base · style base-lyraOpen under the product theme

In a card

One bounded panel with a heading fixed above the region, so maxHeight makes only the card body overflow and scroll.

Theme
Density
Text
Status
scroll-area-in-a-card · base base · style base-lyraOpen under the product theme

Accessibility

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.

KeyActionNotes
TabMoves focus onto the viewport when it is scrollableOne tab stop for the region. Base UI drops it when there is nothing to scroll.
Shift+TabMoves focus back out of the viewportA single stop in reverse too, so focus does not step through the rails.
Arrow keysScroll the focused viewportThe browser's own behaviour on a focused scroll container, along the allowed axes.
Page Up, Page DownScroll the focused viewport by a pageThe browser's default, not a key this component binds.
Home, EndScroll to the start or end of the focused viewportAgain the browser's default on a scroll container.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

Data attributes

AttributeConditionValue
data-slotOn the root and every inner part, alwaysscroll-area, scroll-area-viewport, scroll-area-scrollbar, scroll-area-thumb, scroll-area-corner
data-orientationOn a scrollbar, set by Base UIvertical or horizontal
data-hovering, data-scrollingOn a scrollbar while hovered or while the region scrollsWhat 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.

  • Card draws a bounded surface but grows with its content rather than scrolling it.
  • Sheet is an overlay owning its own scroll, not an inline region.
  • Table is content a scroll area may wrap to scroll sideways.

On this page