Skip to content

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.

tsx
import { ScrollArea } from '@adecore/ui';
tsx
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:

tsx
<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:

tsx
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 ​

PropTypeDefault
axisScrollAreaAxis'y''y' | 'x' | 'both'. The other direction clips.
classNamestringOn the outer box.
refRef<HTMLDivElement>The outer box.
viewportClassNamestringOn the viewport.
viewportRefRef<HTMLElement>The viewport, the element that scrolls.
coveredBottomnumberPixels of the bottom edge a layer over the area covers, such as a docked input. The bars end above it.
renderReactElement | functionReplaces the viewport element.
childrenReactNode

ScrollAreaProps and ScrollAreaAxis are exported types.

Licenses and third-party notices are listed in each package.