Scroll area
A scrolling region with a slim scrollbar on the border color, for sidebar, list, and detail panes.
Scroll area gives one pane its own scrolling, so a sidebar, a list, and a detail view each
scroll independently instead of the whole page moving. It wraps Radix Scroll Area, which
hides the browser's scrollbar and draws a slim one in its place, with a thumb on bg-border-strong.
Scrolling itself stays native, so wheel, touch, keyboard, and find in page work as they do
anywhere else.
Usage
import { ScrollArea } from '@/components/ui/scroll-area';
<ScrollArea className="h-80">
{/* long list */}
</ScrollArea>The area needs a height to scroll against, so set one on it (h-80) or put it in a parent
that has one, such as a flex column with min-h-0. A max-h-* on the area alone does not
limit it, because the viewport fills the area and the area then grows with its content. Use
one area per pane, and keep it off a pane that is as tall as its content. viewportProps go
on the scrolling viewport rather than the area, such as { tabIndex: 0, "aria-label": "Activity" } for a pane with nothing focusable inside.
viewportRef gives you the viewport itself, the element that scrolls, for code that reads or
sets its position, like opening a timeline a few weeks before today or keeping a log pinned to
its end. A ref on ScrollArea lands on the outer area, which does not scroll.
const viewport = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
viewport.current?.scrollTo({ top: viewport.current.scrollHeight });
}, [entries]);
<ScrollArea className="h-80" viewportRef={viewport}>
{/* log entries */}
</ScrollArea>Scrollbar visibility
The scrollbar shows while the pointer is over the area or while it scrolls, and fades out
600ms after. Radix calls this the hover type, and it is the default. Pass type to change
it on the area, as on Radix Scroll Area. scroll shows the bar only while scrolling, auto
shows it whenever the content overflows, and always shows it all the time.
scrollHideDelay sets the fade-out delay in milliseconds.
The bar is 10px wide, and its thumb is a rounded bg-border-strong bar that follows the look's
border color. The thumb can be dragged.
Scrolling sideways
orientation picks the bars the area draws, and the area scrolls only along an axis that has
one. vertical is the default. horizontal is for a sideways strip, like a timeline or a row of
cards, and both is for content that overflows both ways, like a wide canvas. Give sideways
content a width of its own, such as w-max.
import { ScrollArea } from '@/components/ui/scroll-area';
<ScrollArea orientation="horizontal" className="w-full whitespace-nowrap">
<div className="flex w-max gap-4 p-6">{/* wide content */}</div>
</ScrollArea>ScrollBar is still exported, for a bar you place yourself. The area draws its own from
orientation, so add one only alongside an orientation that does not already draw it.
Props
Scroll area takes the props of Radix Scroll Area's root, such as type and scrollHideDelay,
plus these.
| Prop | Type | Default | What it does |
|---|---|---|---|
orientation | "vertical" | "horizontal" | "both" | "vertical" | Which bars to draw, and so which ways the area scrolls. |
viewportRef | Ref<HTMLDivElement> | A ref to the viewport, the element that scrolls. | |
viewportProps | Radix Viewport props | Props for the viewport, like tabIndex and aria-label. | |
className | string | Added to the outer area. Give it the height to scroll against. |
Accessibility
- The viewport is the browser's own scroll container, so the mouse wheel, touch, Page Up and Page Down, the arrow keys, and find in page all scroll it, and the content stays in the reading order.
- The viewport shows a 2px ring on
:focus-visible, and in forced colors that ring is repainted by the system. Radix does not give the viewport atabindex, so whether Tab reaches it depends on the browser. Current Chrome focuses a scroll container that has no focusable content, and other browsers may not. When the content has nothing focusable, passviewportProps={{ tabIndex: 0, "aria-label": "…" }}so every browser gives the keyboard a stop to scroll from, with a name. - The scrollbar and thumb are not exposed to assistive tech, and there is no role or label for them. Scrolling is announced through the content, as with a native scroller.
- The thumb draws in
CanvasTextin forced colors, so it stays visible in Windows high contrast. - The scrollbar has
touch-none, so a finger drag on the bar moves the thumb, and a drag on the content scrolls it natively. - The scrollbar has no color transition under reduced motion, and scrolling is not animated.
When to use something else
- Native overflow (
overflow-y-auto) when you do not need a styled bar. It is lighter, has no wrapper, and gets keyboard focus and the platform's scrollbar for free. Use Scroll area when the browser's bar clashes with the look, or when the same list should look alike on every platform. - Resizable when panes should be resizable. Its panels scroll natively, so place a Scroll area inside one when you want this bar.
- Table for a wide table, which scrolls itself.
Install
shadcn add @gesso/scroll-area