ScrollArea
A box that scrolls under thin bars of its own. The bars float over the content and take no width. They are hidden at rest and show while the box scrolls; a bar the pointer reaches stays until the pointer leaves it.
import { ScrollArea } from '@adecore/ui';import { ScrollArea } from '@adecore/ui';
const FILES = [
'README.md',
'package.json',
'src/index.ts',
'src/theme.css',
'src/ScrollArea.tsx',
'src/Tooltip.tsx',
'src/IconButton.tsx',
'src/format/date.ts'
];
const LINES = Array.from(
{ length: 40 },
(_, index) => `${String(index + 1).padStart(3)} export function step${index}(input: Input, options: Options) { return run(input, options); }`
);
export default function ScrollAreaDemo() {
return (
<div className="grid w-full gap-4 md:grid-cols-2">
<ScrollArea render={<ul />} aria-label="Files" className="h-60 rounded-lg border border-border bg-surface" viewportClassName="py-1">
{Array.from({ length: 60 }, (_, index) => (
<li key={index} className="flex gap-2.5 px-3.5 py-1 text-sm text-text">
<span className="w-7 shrink-0 text-right text-text-faint tabular-nums">{index + 1}</span>
{FILES[index % FILES.length]}
</li>
))}
</ScrollArea>
<ScrollArea
axis="both"
render={<pre />}
className="h-60 rounded-lg border border-border bg-surface"
viewportClassName="px-3.5 py-2.5 font-mono text-code text-text-muted"
>
{LINES.join('\n')}
</ScrollArea>
</div>
);
}The element that scrolls is a native scroller with its own bar hidden. Wheel, keyboard, scrollIntoView, focus and a virtualizer work on it as on any scrolling element.
className sizes the outer box, which holds the bars: give it a height, a max-h-* or a place in a flex column. Padding and the layout of the content go in viewportClassName. Every other prop lands on the viewport, so role, tabIndex, onScroll and aria-* belong to the element that scrolls.
render swaps the viewport for another element, for a list or a <pre> that has to be the element that scrolls:
<ScrollArea render={<ul />} aria-label="Files" className="max-h-60" viewportClassName="p-1">
{files.map((file) => (
<li key={file}>{file}</li>
))}
</ScrollArea>The bars
A bar is a 6 pixel pill, 8 pixels with a faint track while the pointer is on it or drags it. Its hit area is 12 pixels wide along the edge, 20 pixels on a touch screen. Dragging the thumb scrolls along with it, and a press on the track pages toward the pointer. Where both bars show, each stops short of the corner. A bar whose direction has nothing to scroll stays hidden.
The colors are the --scrollbar-thumb, --scrollbar-thumb-hover, --scrollbar-thumb-active and --scrollbar-track tokens of the theme. A scroller that cannot be wrapped, such as one an editor or a list library creates, gets the same bars from useOverlayScrollbars.
A virtualized list
viewportRef hands over the element that scrolls:
const viewport = useRef<HTMLElement>(null);
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => viewport.current,
estimateSize: () => 28
});
return (
<ScrollArea viewportRef={viewport} className="h-full">
<div style={{ height: virtualizer.getTotalSize() }}>…</div>
</ScrollArea>
);Props
| Prop | Type | Default | |
|---|---|---|---|
axis | ScrollAreaAxis | 'y' | 'y' | 'x' | 'both'. The other direction clips. |
className | string | On the outer box. | |
ref | Ref<HTMLDivElement> | The outer box. | |
viewportClassName | string | On the viewport. | |
viewportRef | Ref<HTMLElement> | The viewport, the element that scrolls. | |
coveredBottom | number | Pixels of the bottom edge a layer over the area covers, such as a docked input. The bars end above it. | |
render | ReactElement | function | Replaces the viewport element. | |
children | ReactNode |
ScrollAreaProps and ScrollAreaAxis are exported types.