Scroll Area
A token-aware, accessible scroll container for vertical, horizontal, and two-axis content.
Scroll Area
ScrollArea creates a styled, keyboard-focusable scroll viewport. children can be any React content: text, an article, cards, a table, a list, a canvas, or a custom layout. The component does not require the content to be an array or a particular HTML element.
Installation
import { ScrollArea } from '@mosmox/ui';
import '@mosmox/ui/styles.css';Basic usage
<ScrollArea aria-label="Recent activity" height={280}>
<article>
<h2>Article title</h2>
<p>Any content that exceeds the viewport can be scrolled.</p>
</article>
</ScrollArea>The root container receives the supplied HTML attributes. The nested viewport is focusable with Tab by default and supports the browser's Arrow, Page Up/Down, Home, and End scrolling keys. Add aria-label or aria-labelledby when the surrounding UI does not already provide an accessible name.
Orientation
<ScrollArea orientation="vertical" height={240}>{content}</ScrollArea>
<ScrollArea orientation="horizontal" width={480}>{wideContent}</ScrollArea>
<ScrollArea orientation="both" width={480} height={240}>{gridContent}</ScrollArea>| Value | Behavior |
|---|---|
vertical | Enables Y-axis scrolling; default. |
horizontal | Enables X-axis scrolling. |
both | Enables X- and Y-axis scrolling. |
Scrollbar visibility
<ScrollArea scrollbar="auto">{content}</ScrollArea>
<ScrollArea scrollbar="always">{content}</ScrollArea>
<ScrollArea scrollbar="hover">{content}</ScrollArea>
<ScrollArea scrollbar="hidden">{content}</ScrollArea>| Value | Behavior |
|---|---|
auto | Browser-controlled scrollbar visibility; default. |
always | Keeps the scrollbar track available. |
hover | Shows the thumb while the viewport is hovered or focused. |
hidden | Hides the scrollbar visually while scrolling remains available. |
Use an alternative visual cue, such as fadeEdges, when choosing hidden.
Size and visual variant
<ScrollArea size="sm" variant="soft">{content}</ScrollArea>
<ScrollArea size="lg" variant="bordered">{content}</ScrollArea>size changes scrollbar thickness: sm is 6px, default is 8px, and lg is 12px. variant changes the root appearance:
plain— default surface without an additional border or padding.bordered— adds a border and control radius.soft— adds a soft background, radius, and 8px padding.
Dimensions and edge fades
height, maxHeight, width, and maxWidth accept any corresponding React CSS value. Numbers are converted to pixels; strings can contain any valid CSS size such as %, rem, vh, min(), or calc().
<ScrollArea height={320} maxWidth="min(100%, 720px)" fadeEdges>
{content}
</ScrollArea>fadeEdges adds gradient overlays at the scrollable edges. It is false by default and follows the active orientation.
Events and edge detection
<ScrollArea
height={320}
scrollThreshold={12}
onScroll={(event) => console.log(event.currentTarget.scrollTop)}
onReachStart={() => console.log('reached start')}
onReachEnd={() => loadMore()}
>
{items}
</ScrollArea>onScrollreceives the native ReactUIEvent<HTMLDivElement>from the internal viewport.onReachStartfires when scrolling enters the start edge.onReachEndfires when scrolling enters the end edge.scrollThresholdis the edge tolerance in pixels and defaults to2.
Edge callbacks fire once per arrival. They become eligible again after the viewport leaves that edge, so holding at the end does not repeatedly call onReachEnd. For horizontal, start/end are the left/right edges. For both, the callback fires only when both axes are at the corresponding edge.
Imperative controls
The forwarded ref exposes a ScrollAreaHandle for controlling the internal viewport:
import { useRef } from 'react';
import { ScrollArea, type ScrollAreaHandle } from '@mosmox/ui';
const ref = useRef<ScrollAreaHandle>(null);
<>
<button onClick={() => ref.current?.scrollToTop()}>Top</button>
<button onClick={() => ref.current?.scrollToBottom('smooth')}>Bottom</button>
<button onClick={() => ref.current?.scrollTo({ top: 80, behavior: 'smooth' })}>
Scroll to 80px
</button>
<ScrollArea ref={ref} height={300}>{content}</ScrollArea>
</>| Member | Description |
|---|---|
element | The internal HTMLDivElement viewport, or null before mount. |
scrollTo(options) | Calls the viewport's native scrollTo with ScrollToOptions. |
scrollToTop(behavior?) | Scrolls vertically to top: 0. |
scrollToBottom(behavior?) | Scrolls vertically to the maximum content height. |
scrollToStart(behavior?) | Scrolls horizontally to left: 0. |
scrollToEnd(behavior?) | Scrolls horizontally to the maximum content width. |
The optional behavior is auto or smooth; the convenience methods default to smooth. viewportRef is separate from the forwarded component ref and gives direct access to the internal viewport element:
const viewportRef = useRef<HTMLDivElement>(null);
<ScrollArea viewportRef={viewportRef}>{content}</ScrollArea>ConfigProvider defaults
The provider can set defaults for every ScrollArea. Instance props take precedence over provider values.
<ConfigProvider
components={{
scrollArea: {
orientation: 'vertical',
scrollbar: 'hover',
size: 'sm',
variant: 'soft',
fadeEdges: true,
},
}}
>
<ScrollArea height={240}>{content}</ScrollArea>
</ConfigProvider>Supported defaults are orientation, scrollbar, size, variant, and fadeEdges. If neither the provider nor the instance supplies a value, the defaults are vertical, auto, default, plain, and false respectively.
API
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Any content rendered inside the scrollable viewport. |
orientation | 'vertical' | 'horizontal' | 'both' | 'vertical' | Scroll axis or axes. |
scrollbar | 'auto' | 'always' | 'hover' | 'hidden' | 'auto' | Scrollbar visibility behavior. |
size | 'sm' | 'default' | 'lg' | 'default' | Scrollbar thickness. |
variant | 'plain' | 'bordered' | 'soft' | 'plain' | Root visual treatment. |
height | CSSProperties['height'] | — | Root height; numbers are pixels. |
maxHeight | CSSProperties['maxHeight'] | — | Root maximum height; numbers are pixels. |
width | CSSProperties['width'] | 100% | Root width; numbers are pixels. |
maxWidth | CSSProperties['maxWidth'] | — | Root maximum width; numbers are pixels. |
fadeEdges | boolean | false | Shows gradient cues at scrollable edges. |
viewportRef | Ref<HTMLDivElement> | — | Ref for the internal scroll viewport. |
onScroll | (event: UIEvent<HTMLDivElement>) => void | — | Receives viewport scroll events. |
onReachStart | () => void | — | Called once when the start edge is entered. |
onReachEnd | () => void | — | Called once when the end edge is entered. |
scrollThreshold | number | 2 | Edge tolerance in pixels. |
className / style | Standard React values | — | Applied to the root container. |
| Other HTML/ARIA/data props | Standard HTMLAttributes<HTMLDivElement> | — | Forwarded to the root container. onScroll is the ScrollArea callback above and is handled by the viewport. |
Accessibility and performance
The internal viewport is keyboard-focusable by default and shows a visible focus ring. Pass tabIndex={-1} when the viewport should not enter the normal tab order, or another value when your interaction model requires it. The component uses native scrolling and CSS overflow, avoids rendering a second copy of the content, and uses overscroll-behavior: contain to prevent scroll chaining to the page.