Components

Scroll area

Radix ScrollArea wrapper with thin native-like scrollbars in Yuno tokens (w-2.5 track, bg-border thumb, rounded-full). Auto-hides when there's nothing to scroll. Renders vertical only by default; add a second <ScrollBar orientation='horizontal' /> for horizontal or both axes.

Updated Jul 17, 2026 by Leonardo Posada

Anatomy

Four parts: the Root (positioning + overflow-hidden), the Viewport (what actually scrolls), the ScrollBar (track + thumb), and — when both axes scroll — the Corner where the two scrollbars meet.

Tags
v1.2.0-beta.50
v1.2.0-beta.49
v1.2.0-beta.48
v1.2.0-beta.47
v1.2.0-beta.46
v1.2.0-beta.45
v1.2.0-beta.44
v1.2.0-beta.43
v1.2.0-beta.42
v1.2.0-beta.41
v1.2.0-beta.40
v1.2.0-beta.39
v1.2.0-beta.38
v1.2.0-beta.37
v1.2.0-beta.36
  1. 1
    ScrollArea (Root)

    Positioned relative + overflow-hidden. Set the container size via className (h-72 w-48, max-h-96, etc.) — without a bounded height/width, the scrollbar never appears.

  2. 2
    Viewport

    h-full w-full rounded-[inherit]. Children live inside this — the actual scrollable canvas. Radix wraps it so the native scrollbar can be hidden.

  3. 3
    ScrollBar

    The custom scrollbar track. w-2.5 (10px) with a border-l-transparent hairline; renders vertical by default. For horizontal scroll, add another <ScrollBar orientation='horizontal' />.

  4. 4
    Thumb (inside ScrollBar)

    flex-1 rounded-full bg-border. The draggable indicator. Muted intentionally so it doesn't compete with content.

  5. 5
    Corner

    The 10×10 square where a vertical and a horizontal scrollbar meet. Only visible when both scrollbars are rendered simultaneously.

Variants

One primitive, three practical arrangements: vertical only (default), horizontal only, and both axes. Add a ScrollBar per axis you want.

Vertical (default)
Tags
v1.2.0-beta.50
v1.2.0-beta.49
v1.2.0-beta.48
v1.2.0-beta.47
v1.2.0-beta.46
v1.2.0-beta.45
v1.2.0-beta.44
v1.2.0-beta.43
v1.2.0-beta.42
v1.2.0-beta.41
v1.2.0-beta.40
v1.2.0-beta.39
v1.2.0-beta.38
v1.2.0-beta.37
v1.2.0-beta.36
<ScrollArea className="h-72 w-48 rounded-md border">
  <div className="p-4">
    <div className="pb-2 text-sm font-medium">Tags</div>
    {tags.map((tag, i) => (
      <React.Fragment key={tag}>
        {i > 0 && <Separator className="my-2" />}
        <div className="text-sm">{tag}</div>
      </React.Fragment>
    ))}
  </div>
</ScrollArea>
Horizontal
Photo by Ornella Binni
Photo by Tom Byrom
Photo by Vladimir Malyavko
Photo by Aleksandar Popovski
Photo by Casey Horner
<ScrollArea className="w-96 whitespace-nowrap rounded-md border">
  <div className="flex w-max gap-4 p-4">
    {photos.map((photo) => (
      <figure key={photo.author} className="shrink-0">
        <div className="overflow-hidden rounded-md">
          <img src={photo.src} className="aspect-[3/4] w-40 object-cover" />
        </div>
        <figcaption className="pt-2 text-xs text-muted-foreground">
          Photo by <span className="font-semibold text-foreground">{photo.author}</span>
        </figcaption>
      </figure>
    ))}
  </div>
  <ScrollBar orientation="horizontal" />
</ScrollArea>
Both axes
Wide + tall content
v1.2.0-beta.50 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.49 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.48 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.47 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.46 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.45 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.44 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.43 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.42 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.41 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.40 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.39 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.38 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.37 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.36 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.35 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.34 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.33 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.32 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
v1.2.0-beta.31 — extra content that pushes the row past the viewport width so the horizontal scrollbar appears too
<ScrollArea className="h-56 w-96 rounded-md border">
  <div className="w-[720px] p-4">
    {/* wide + tall content that scrolls both axes */}
  </div>
  <ScrollBar orientation="horizontal" />
</ScrollArea>

States

Scrollbar states: idle (hidden when content fits; auto-appears on hover / touch when scrollable — the 'hover' auto-hide behavior can be overridden with type='always'), hover (thumb picks up bg-border tone), dragging (thumb tracks the pointer while user drags).

Bounded size or nothing scrolls
ScrollArea only surfaces a scrollbar when its content exceeds its own bounds. Always give the Root a fixed or max height (vertical) / width (horizontal). Without a bound the primitive still renders — you just won't see a scrollbar because there's nothing to scroll.

Recipes

Ready-to-copy compositions covering the most common Yuno usages of this atom.

Versions list (Figma canonical)

The Figma spec pattern: a short vertical list with a header + Separator between rows. Height-constrained (h-72 w-48) so the scrollbar appears when the list overflows.

Tags
v1.2.0-beta.50
v1.2.0-beta.49
v1.2.0-beta.48
v1.2.0-beta.47
v1.2.0-beta.46
v1.2.0-beta.45
v1.2.0-beta.44
v1.2.0-beta.43
v1.2.0-beta.42
v1.2.0-beta.41
v1.2.0-beta.40
v1.2.0-beta.39
v1.2.0-beta.38
v1.2.0-beta.37
v1.2.0-beta.36
<ScrollArea className="h-72 w-48 rounded-md border">
  <div className="p-4">
    <div className="pb-2 text-sm font-medium">Tags</div>
    {tags.map((tag, i) => (
      <React.Fragment key={tag}>
        {i > 0 && <Separator className="my-2" />}
        <div className="text-sm">{tag}</div>
      </React.Fragment>
    ))}
  </div>
</ScrollArea>
Horizontal photo gallery

Horizontal-only scroll: a row of aspect-ratio figures inside a fixed-width container. Add a second ScrollBar with orientation='horizontal' — the vertical one auto-hides when there's no vertical overflow.

Photo by Ornella Binni
Photo by Tom Byrom
Photo by Vladimir Malyavko
Photo by Aleksandar Popovski
Photo by Casey Horner
<ScrollArea className="w-96 whitespace-nowrap rounded-md border">
  <div className="flex w-max gap-4 p-4">
    {photos.map((photo) => (
      <figure key={photo.author} className="shrink-0">
        <img src={photo.src} className="aspect-[3/4] w-40 rounded-md object-cover" />
        <figcaption className="pt-2 text-xs">Photo by <b>{photo.author}</b></figcaption>
      </figure>
    ))}
  </div>
  <ScrollBar orientation="horizontal" />
</ScrollArea>
Long list inside a Popover

A common Dashboard pattern: a filter or account picker inside a Popover shouldn't push the layout when the list grows. Wrap the list in ScrollArea with a max-h to cap the popover height.

<PopoverContent className="w-64 p-0">
  <ScrollArea className="max-h-60">
    <div className="p-2">
      {accounts.map((a) => (
        <button
          key={a.id}
          className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
        >
          {a.name}
        </button>
      ))}
    </div>
  </ScrollArea>
</PopoverContent>
Table with horizontal scroll

Wide table on a narrow viewport: wrap the Table in a horizontal-only ScrollArea so the layout doesn't force horizontal page scroll.

Transaction ID
Merchant
Amount
Currency
Provider
Status
Created at
Updated at
tx_9f8a2b
Tiendamia BR
1.234,56
BRL
Adyen
Succeeded
2026-07-17 14:22
2026-07-17 14:23
tx_9f8a2b
Tiendamia BR
1.234,56
BRL
Adyen
Succeeded
2026-07-17 14:22
2026-07-17 14:23
tx_9f8a2b
Tiendamia BR
1.234,56
BRL
Adyen
Succeeded
2026-07-17 14:22
2026-07-17 14:23
tx_9f8a2b
Tiendamia BR
1.234,56
BRL
Adyen
Succeeded
2026-07-17 14:22
2026-07-17 14:23
tx_9f8a2b
Tiendamia BR
1.234,56
BRL
Adyen
Succeeded
2026-07-17 14:22
2026-07-17 14:23
<ScrollArea className="w-full whitespace-nowrap rounded-md border">
  <Table className="w-max min-w-full">
    {/* many columns */}
  </Table>
  <ScrollBar orientation="horizontal" />
</ScrollArea>

Import

Two exports from @/components/ui/scroll-area.

Add ScrollBar only when you need horizontal (or both axes).
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";

Props

ScrollArea and ScrollBar forward all Radix props. The most useful ones for prototypes:

PropTypeDefaultDescription
ScrollArea.classNamestringMerged onto the Root. This is where you set the bounded size (h-*, w-*, max-h-*). Also common: rounded-md border to give the region a surface.
ScrollArea.type"auto" | "always" | "scroll" | "hover""hover"Radix scrollbar visibility strategy. 'hover' auto-hides until the user hovers; 'always' keeps the scrollbar visible; 'scroll' shows only while scrolling; 'auto' matches the OS setting.
ScrollArea.scrollHideDelaynumber600ms before the scrollbar hides after scrolling stops. Only relevant for type='scroll' / 'hover'.
ScrollArea.dir"ltr" | "rtl"Reading direction. Forwarded to Radix; affects which side the vertical scrollbar renders on.
ScrollBar.orientation"vertical" | "horizontal""vertical"Which axis this scrollbar controls. Vertical is rendered automatically by <ScrollArea>. Add a second <ScrollBar orientation='horizontal' /> as a child of ScrollArea when you also need horizontal.
ScrollBar.classNamestringMerged onto the scrollbar track. Rarely needed — the DS tokens (w-2.5 / bg-border thumb) already match. Avoid overriding colors.

When to use

  • Constrained regions inside Cards, Sheets, Popovers or Dialogs where a native browser scrollbar would look inconsistent.
  • Horizontal scroll surfaces (tables, image rows, tag rows) where you want the same thin scrollbar as verticals.
  • Any list that should scroll independently without moving the surrounding page.

When not to use

  • Page-level scroll — the browser (and the kit's docked shell) already handles that.
  • A single overflow-x row where a plain overflow-x-auto is enough and no custom scrollbar is needed.
  • Content the user really needs to see all of (accessibility) — provide a way to expand or paginate, not just scroll.

Usage

Do
  • Bound the Root with an explicit height / width / max-*.
  • Add one ScrollBar per axis you actually need — omit the second one to keep the corner clean.
  • Keep the thumb color at bg-border — it should feel like part of the surface, not a UI element.
  • Wrap the immediate content in the same padding you would use on a Card (p-4 default, p-2 dense).
Don't
  • Don't nest ScrollAreas inside each other — the second scrollbar becomes unreachable.
  • Don't rely on ScrollArea for critical content — long lists still need pagination or 'load more'.
  • Don't override the thumb to a bright color — it competes with the content.
  • Don't wrap page-level content in a ScrollArea — the docked shell already scrolls internally.

Related

Cross-links to atoms and patterns you may reach for next.

  • SheetSheets have their own scroll — only wrap inner content in ScrollArea when you need a nested, capped region.
  • PopoverCap Popover content growth by wrapping the list in a max-h ScrollArea.
  • TableWrap wide tables in a horizontal ScrollArea so the page itself doesn't force horizontal scroll.
  • CommandCommand palette lists already use their own scroll — you don't need a ScrollArea inside.