Scroll Area

A scrolling region with Koala's own scrollbar: an ink pill that overlays the content, shows while you are over it, fades rather than blinks, and looks the same on every OS and theme. The scrolling itself stays native.

Releases

  • v2.4.1
    PatchSep 24
  • v2.4.0
    MinorSep 18
  • v2.3.2
    PatchSep 03
  • v2.3.1
    PatchAug 27
  • v2.3.0
    MinorAug 20
  • v2.2.4
    PatchAug 06
  • v2.2.3
    PatchJul 30
  • v2.2.0
    MinorJul 16
  • v2.1.0
    MinorJun 25
  • v2.0.0
    MajorJun 04
  • v1.9.2
    PatchMay 21
  • v1.9.0
    MinorMay 07

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add scroll-area

Usage#

usage.tsxTSX
import { ScrollArea } from "@/components/ui/scroll-area"export function Example() {  return (    <ScrollArea className="h-72">      {/* anything taller than 288px */}    </ScrollArea>  )}

Give the root a height (h-72, max-h-96, or flex-1 min-h-0 in a flex column) and the viewport fills it. className styles the root; the viewport inherits its radius, so a rounded frame clips the content and the focus ring to its corners.

Edge fade#

fade dissolves the edge that still has content beyond it, so the region says there is more before anyone hovers it. It is the scroll-fade utility, pure CSS: the edge you are scrolling toward fades, and the one you reach sharpens.

No fade
  • v2.4.1
    PatchSep 24
  • v2.4.0
    MinorSep 18
  • v2.3.2
    PatchSep 03
  • v2.3.1
    PatchAug 27
  • v2.3.0
    MinorAug 20
  • v2.2.4
    PatchAug 06
  • v2.2.3
    PatchJul 30
  • v2.2.0
    MinorJul 16
  • v2.1.0
    MinorJun 25
  • v2.0.0
    MajorJun 04
  • v1.9.2
    PatchMay 21
  • v1.9.0
    MinorMay 07
fade
  • v2.4.1
    PatchSep 24
  • v2.4.0
    MinorSep 18
  • v2.3.2
    PatchSep 03
  • v2.3.1
    PatchAug 27
  • v2.3.0
    MinorAug 20
  • v2.2.4
    PatchAug 06
  • v2.2.3
    PatchJul 30
  • v2.2.0
    MinorJul 16
  • v2.1.0
    MinorJun 25
  • v2.0.0
    MajorJun 04
  • v1.9.2
    PatchMay 21
  • v1.9.0
    MinorMay 07

Horizontal#

orientation="horizontal" draws the bar along the bottom and lets the content grow to its own width (w-max on the row). The photos sit 8px inside the frame, on 8px corners under its 16px ones, each with the thin image outline.

The cabin at dusk
The cabin at dusk
Firepit
Firepit
The deck
The deck
Dining room
Dining room
The great room
The great room
Kitchen
Kitchen

Drag with the mouse#

draggable lets a mouse pull the content the way a finger already does: press, drag, let go and it glides on. For a strip of cards or photos, where people reach for the content rather than the bar. A press that barely moves is still a click, and a real drag swallows the click it ends on, so a card link never opens by accident. Touch and pen keep their native scroll, and the glide is skipped under reduced motion.

The cabin at dusk
The cabin at dusk
Firepit
Firepit
The deck
The deck
Dining room
Dining room
The great room
The great room
Kitchen
Kitchen

Both axes#

orientation="both" draws both bars and a corner where they meet, for content wider and taller than its frame: a year of figures, a schedule, a canvas.

Active users (k)JanFebMarAprMayJunJulAugSepOctNovDec
North America41.242.744.245.847.348.850.351.953.454.956.458.0
Europe33.835.136.337.638.840.141.342.643.845.146.347.6
Latin America12.412.913.313.814.214.715.215.616.116.517.017.4
Asia Pacific27.628.629.630.731.732.733.734.735.836.837.838.8
Middle East6.16.36.66.87.07.27.57.77.98.18.48.6
Africa4.34.54.64.84.95.15.35.45.65.75.96.1
Oceania3.23.33.43.63.73.83.94.04.14.34.44.5

When the bar shows#

type is Radix's: hover (the default) shows the bar while the pointer is over the region, scroll only while it is scrolling, and always whenever the content overflows. However it appears, it fades in and out, and it thickens a step when you reach for it, the way a native overlay bar does.

hover · While the pointer is over it

Every component ships as source you own, installed with one command and updated without losing your edits. Tokens drive the four themes, so a change to a color role reaches every surface at once. Density is one knob for a whole app shell, and the control sizes follow the container they sit in. The docs show every variant, live, in every theme.

scroll · Only while scrolling

Every component ships as source you own, installed with one command and updated without losing your edits. Tokens drive the four themes, so a change to a color role reaches every surface at once. Density is one knob for a whole app shell, and the control sizes follow the container they sit in. The docs show every variant, live, in every theme.

always · Always, whenever it overflows

Every component ships as source you own, installed with one command and updated without losing your edits. Tokens drive the four themes, so a change to a color role reaches every surface at once. Density is one knob for a whole app shell, and the control sizes follow the container they sit in. The docs show every variant, live, in every theme.

The viewport#

viewportRef is the element that actually scrolls: read scrollTop, call scrollTo, or observe it. Here it drives a button that appears once you are away from the top and takes you back smoothly.

  1. 09:00Build started on main
  2. 09:15Type check passed
  3. 09:30Preview deployed
  4. 09:45Checks green, promoted to production
  5. 10:00Build started on main
  6. 10:15Type check passed
  7. 10:30Preview deployed
  8. 10:45Checks green, promoted to production
  9. 11:00Build started on main
  10. 11:15Type check passed
  11. 11:30Preview deployed
  12. 11:45Checks green, promoted to production
  13. 12:00Build started on main
  14. 12:15Type check passed
  15. 12:30Preview deployed
  16. 12:45Checks green, promoted to production
  17. 13:00Build started on main
  18. 13:15Type check passed
  19. 13:30Preview deployed
  20. 13:45Checks green, promoted to production
  21. 14:00Build started on main
  22. 14:15Type check passed
  23. 14:30Preview deployed
  24. 14:45Checks green, promoted to production

Focus rings#

The viewport carries scroll-p-2: when Tab (or a focus()) scrolls a control into view, the browser stops 8px short of the edge rather than flush against it, so the control's focus ring (2px on a 2px offset) is never cut by the viewport. It changes where focus and scrollIntoView land, never the layout, and Chromium 102 honors it. The first and last controls sit against the content's own edge, which scroll padding cannot move, so give the content a little padding (p-2 here). To change the amount, target the viewport: [&>[data-slot=scroll-area-viewport]]:scroll-p-4.

API reference#

ScrollArea

The root, viewport, bars and corner in one, over Radix ScrollArea.Root; give it a height and the viewport fills it. Forwards the Radix root props and accepts className (on the root), merged last.

PropTypeDefaultDescription
orientation"vertical" | "horizontal" | "both""vertical"Which way the content scrolls, and so which bars draw. A vertical-only area unwraps Radix's table box, so truncate works inside.
fadebooleanfalseFades the edge that still has content beyond it (the scroll-fade utility; vertical edges when both).
draggablebooleanfalseLets a mouse pull the content, with a glide on release. A press that barely moves is still a click; touch and pen scroll natively.
viewportRefReact.Ref<HTMLDivElement>-A ref to the scrolling element itself, to read scrollTop or call scrollTo.
type"hover" | "scroll" | "always" | "auto""hover"When the bar shows: while hovered, while scrolling, always, or whenever the content overflows.
scrollHideDelaynumber600Milliseconds before the bar fades after the pointer leaves or scrolling stops.
dir"ltr" | "rtl"-Reading direction, which sets the side of the vertical bar.
Data attributeDescription
data-statevisible or hidden, on each scrollbar; it fades between them.
data-orientationvertical or horizontal, on each scrollbar.
data-overflowingOn the viewport with draggable, while the content overflows (shows the grab cursor).
data-draggingOn the viewport with draggable, while a mouse pull is live.

FAQ#

The site's own scrollbars are hidden, so a plain `overflow-y-auto` box with the `scroll-fade` utility is the lighter option when the fade is cue enough: menus, pickers, chat logs. Use Scroll Area when the bar itself should show, for instance a long panel people drag through.