ScrollArea
scroll-areaAdds slim custom scrollbars for vertical, horizontal, or two-axis overflow.
Usage
Vertical scroll
Default orientation=vertical, a thin scroll bar is displayed when the content exceeds the height of the container.
<ScrollArea className="h-48 w-72 border border-border p-4">
{/* Content that exceeds the height of the container */}
</ScrollArea>Horizontal scrolling
orientation=horizontal, with a row of flex cards for horizontal browsing.
<ScrollArea orientation="horizontal" className="w-72 border border-border p-4">
<div className="flex gap-3">{/* Cards */}</div>
</ScrollArea>Capped scrolling
Use max-h-* for grow-then-scroll: short content shows no scrollbar and leaves none of the empty space below that a fixed height would.
<ScrollArea className="max-h-40 w-56 border border-border p-4">
{/* Scrolls only past 40; below that the container is as tall as its content */}
</ScrollArea>Bi-directional scrolling
orientation=both, horizontal and vertical scroll bars appear at the same time and corner is added in the lower right corner.
<ScrollArea orientation="both" className="h-48 w-72 border border-border p-4">
{/* wide and tall content */}
</ScrollArea>When to use
Use ScrollArea for constrained content that needs thin, consistent cross-platform scrollbars. It controls scrollbar styling and orientation only. Use Resizable when users should change the area's size, or Viewport for container-query layout changes.
Import
import { ScrollArea } from "@hulianui/ui"Props
| Name | Type | Default | Description |
|---|---|---|---|
| orientation | "vertical" | "horizontal" | "both" | "vertical" | Scroll direction. both renders horizontal and vertical bars plus their corner. |
| className | string | - | Consumer-supplied size constraint on Root, such as h-48 or w-64; without one, content expands instead of scrolling. |
| viewportClassName | string | - | Appended to the inner viewport, which is the element that actually scrolls. className lands on Root and cannot reach the viewport, yet clipping happens there, so pass something like px-1.5 when a flush-mounted control loses its focus ring (#340; see the usage note below). |
Slots
| Slot | Type | Description |
|---|---|---|
| children | ReactNode | Scroll content. |
Example
// Vertical scrolling requires a height constraint
<ScrollArea className="h-48 w-72 border border-border bg-surface p-4">
{/* Long content */}
</ScrollArea>// Horizontal scrolling
<ScrollArea orientation="horizontal" className="w-72">
<div className="flex gap-3">{/* Horizontal cards */}</div>
</ScrollArea>Usage guidelines
- Scrolling requires a size constraint. ScrollArea has no intrinsic size, so constrain it through
classNameor content will expand the container and no scrollbar will appear. Useh-*for a fixed panel, which leaves empty space below short content, andmax-h-*for grow-then-scroll, which stays snug until the cap is reached (w-*/max-w-*horizontally). Before 0.61.0max-h-*silently clipped the overflow, with no scrollbar, no wheel response and no keyboard access; both forms now work (#342). - The scrollbar is an overlay, so the content must reserve its own gutter. The bar is absolutely positioned, takes no layout width, and is
w-2(8px) wide; a horizontal bar is the same height. Give the content at least `pr-2.5` (10px: an 8px bar plus 2px of breathing room), orpb-2.5when scrolling horizontally. A smaller gutter such as the commonpr-1(4px) leaves the bar sitting on top of the rightmost column (#118). This is an explicit convention rather than component behaviour, because only the consumer knows whether the gutter belongs on the content wrapper or on individual columns. - A flush-mounted control loses its focus ring; reserve room with `viewportClassName` (#340). The clipping described below does not only affect content: a
w-fullInputsits flush against the viewport, so the 4px thatring-2 + ring-offset-2paints outside the box falls beyond the viewport and is cut away. The visible result is a focus ring reduced to its top and bottom edges (spare height keeps those two alive). Padding on Root does not help, because the viewport is what clips. PassviewportClassName="px-1.5". The component ships no default gutter, because only you know whether the room belongs on the scroll container or on individual columns.
- Overflow on the undeclared axis is locked (hidden), never silently scrollable. A
verticalviewport getsoverflow-x: hidden, ahorizontalviewport getsoverflow-y: hidden(bothleaves both axes open). Base UI styles the viewport with two-axisoverflow: scrolland hides the native bars, so content even 1px wider than the viewport used to pan sideways under a trackpad swipe with no scrollbar to explain it, and it looked like a broken layout (#287). Content inside averticalarea therefore has to fit its width (w-full/min-w-0/ internal truncation); anything wider is clipped rather than scrollable. Declareorientation="both"when you really want two-axis scrolling.
Related
Layout · AdminLayout · Viewport · Resizable · AspectRatio · FitScreen
Playground
<ScrollArea className="h-48 w-72">
{/* Content */}
</ScrollArea>