ScrollArea
Themed, cross-browser scroll container with custom scrollbars — keeps long content contained without breaking native scroll semantics.
Falls back to native scrolling behaviour; keyboard scrolling (arrow keys, Page Up/Down, Home/End) is preserved.
Preview
tsx
import { ScrollArea } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<ScrollArea
height={240}
width={320}
scrollbars="vertical"
style={{
border: '1px solid var(--cynosure-color-border)',
borderRadius: 'var(--cynosure-radius-md)',
}}
>
<div style={{ padding: '0.75rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{Array.from({ length: 40 }, (_, i) => {
const id = (i + 1).toString().padStart(2, '0');
return (
<div key={`row-${id}`} style={{ fontSize: '0.875rem' }}>
Item #{id} — a short entry in a long list.
</div>
);
})}
</div>
</ScrollArea>
);
}
Horizontal
Section titled “Horizontal”Set scrollbars="horizontal" for an x-axis scroll region — pair with a wide inner container that overflows the viewport width.
Preview
tsx
import { ScrollArea } from '@arshad-shah/cynosure-react';
const tags = [
'design',
'accessibility',
'tokens',
'typography',
'components',
'react',
'vanilla-extract',
'radix-ui',
'storybook',
'astro',
];
export default function Example() {
return (
<ScrollArea
width={360}
height={64}
scrollbars="horizontal"
style={{
border: '1px solid var(--cynosure-color-border)',
borderRadius: 'var(--cynosure-radius-md)',
}}
>
<div style={{ display: 'flex', gap: '0.5rem', padding: '0.75rem', width: 'max-content' }}>
{tags.map((tag) => (
<span
key={tag}
style={{
padding: '0.25rem 0.625rem',
border: '1px solid var(--cynosure-color-border)',
borderRadius: 'var(--cynosure-radius-full)',
fontSize: '0.75rem',
whiteSpace: 'nowrap',
}}
>
#{tag}
</span>
))}
</div>
</ScrollArea>
);
}
Both axes
Section titled “Both axes”The default scrollbars="both" renders thumbs on both axes; a corner element fills the intersection.
Preview
tsx
import { ScrollArea } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<ScrollArea
width={320}
height={220}
scrollbars="both"
style={{
border: '1px solid var(--cynosure-color-border)',
borderRadius: 'var(--cynosure-radius-md)',
}}
>
<div style={{ padding: '0.75rem', width: 560 }}>
{Array.from({ length: 24 }, (_, i) => {
const id = (i + 1).toString().padStart(2, '0');
return (
<p key={`row-${id}`} style={{ margin: 0, fontSize: '0.875rem', lineHeight: 1.7 }}>
Row {id} — this paragraph is wide enough to force horizontal scrolling alongside the
vertical axis.
</p>
);
})}
</div>
</ScrollArea>
);
}
Visibility type
Section titled “Visibility type”The type prop controls when the scrollbars appear: hover (default), auto, always, or scroll.
Preview
tsx
import { ScrollArea } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<ScrollArea
width={320}
height={180}
type="always"
scrollbars="vertical"
style={{
border: '1px solid var(--cynosure-color-border)',
borderRadius: 'var(--cynosure-radius-md)',
}}
>
<div style={{ padding: '0.75rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{Array.from({ length: 30 }, (_, i) => {
const id = (i + 1).toString().padStart(2, '0');
return (
<div key={`row-${id}`} style={{ fontSize: '0.875rem' }}>
Always-visible scrollbar row #{id}
</div>
);
})}
</div>
</ScrollArea>
);
}
PropTypeDefaultDescription
type
"auto"|"always"|"hover"
auto
When scrollbars are visible.
- `auto` (default): browser default — scrollbar appears when content overflows.
- `always`: reserved gutter even when no overflow.
- `hover`: scrollbars become visible on pointer over via CSS.
height
string|number
—
Convenience for the viewport height (number is converted to `px`).
width
string|number
—
Convenience for the viewport width (number is converted to `px`).
children
ReactNode
—
Scrollable content.
scrollbars
"vertical"|"horizontal"|"both"
both
Which scrollbars to render — useful to suppress the orthogonal axis.
Accessibility
Section titled “Accessibility”- Preserves native scroll mechanics — keyboard scrolling (arrow keys,
PageUp/PageDown,Home/End) works without extra wiring. - Custom scrollbars are decorative; the underlying content remains the source of truth for assistive tech.
- Honours
prefers-reduced-motionfor thumb fade transitions. - Pass
aria-label(oraria-labelledby) when the scroll region’s purpose is not clear from surrounding context. - Wheel and pointer scrolling continue to work even when the custom scrollbars are hidden.
Recipes
Section titled “Recipes”- Set
scrollbars="vertical"or"horizontal"to constrain the scroll axis to a single direction. - Combine with
CodeBlockto keep long snippets contained inside a fixed height. - Use
type="always"to keep the scrollbars visible at all times (e.g. macOS users who hide them by default). - Pass
widthandheightdirectly for an inline-sized scroll region without writing extra styles.