Scrollable
A scroll container that fades content out towards edges with more to scroll.
Default
Scrollable wraps its children in a scroll container and fades the content out towards any edge that still has more to scroll. The fades update as you scroll, and disappear once you reach the corresponding edge. By default the content scrolls vertically — constrain the container's size to make it scroll:
Editable example<Scrollable aria-label="Article" style={{ height: "160px", width: "320px" }}>
<Stack gap="s" padding="s">
<Text element="p">Scrollable fades its content towards any edge that still has more to scroll.</Text>
<Text element="p">The fade is drawn with a mask, so it looks correct over any background colour.</Text>
<Text element="p">Set a direction to scroll vertically or horizontally.</Text>
<Text element="p">Choose a fade size to make the effect more or less pronounced.</Text>
<Text element="p">Turn the fade off entirely when you want a plain scroll container.</Text>
</Stack>
</Scrollable>
Best practices
- Use Scrollable when content overflows a fixed-size region and you want a visual hint that there's more to see — lists, sidebars, card rails, chat logs.
- Give the region an
aria-label(oraria-labelledby) when it's a meaningful standalone area of the page, so it's exposed as a landmark and keyboard users can focus and scroll it. - The fade is drawn with a mask rather than an overlay, so it works over any background — no need to match it to the surface colour behind it.
- Don't rely on the fade alone to communicate essential content; it's a hint, not a control.
Horizontal scrolling
Set direction="horizontal" to scroll along the horizontal axis. The fades move to the left and right edges:
Editable example<Scrollable direction="horizontal" aria-label="Cards" style={{ width: "320px" }}>
<div style={{ display: "flex", gap: "12px", padding: "12px" }}>
{Array.from({ length: 8 }).map((_, index) => (
<Card key={index}>
<Stack padding="m" style={{ width: "96px" }}>
<Text element="p">Card {index + 1}</Text>
</Stack>
</Card>
))}
</div>
</Scrollable>
Fade edges
Use the fade prop to control which edges of the scroll axis fade: "both" (default), "start", "end", or "none" for a plain scroll container:
Editable example<Stack direction="horizontal" gap="m">
<Scrollable fade="end" aria-label="End fade only" style={{ height: "120px", width: "200px" }}>
<Stack gap="s" padding="s">
<Text element="p">This container only fades at the end of the scroll axis.</Text>
<Text element="p">The start edge stays fully opaque even when scrolled.</Text>
<Text element="p">Keep scrolling to reach the bottom.</Text>
</Stack>
</Scrollable>
<Scrollable fade="none" aria-label="No fade" style={{ height: "120px", width: "200px" }}>
<Stack gap="s" padding="s">
<Text element="p">This container has the fade disabled entirely.</Text>
<Text element="p">It behaves like a plain scroll container.</Text>
<Text element="p">Keep scrolling to reach the bottom.</Text>
</Stack>
</Scrollable>
</Stack>
Fade size
Use the fadeSize prop to control how large the edge fade is, in pixels. Defaults to 16:
Editable example<Scrollable fadeSize={48} aria-label="Large fade" style={{ height: "160px", width: "320px" }}>
<Stack gap="s" padding="s">
<Text element="p">A larger fade makes the effect more pronounced.</Text>
<Text element="p">Use it for taller regions where a subtle fade would be easy to miss.</Text>
<Text element="p">Smaller values keep more of the content readable.</Text>
<Text element="p">Keep scrolling to reach the bottom.</Text>
</Stack>
</Scrollable>
Responding to scroll
Scrollable forwards its ref to the underlying scroll container and accepts an onScroll handler, so you can read or control the scroll position directly:
Editable example() => {
const [progress, setProgress] = React.useState(0);
return (
<Stack gap="s">
<Text element="p">Scrolled: {progress}%</Text>
<Scrollable
aria-label="Scroll progress example"
style={{ height: "120px", width: "320px" }}
onScroll={(event) => {
const el = event.currentTarget;
const max = el.scrollHeight - el.clientHeight;
setProgress(max > 0 ? Math.round((el.scrollTop / max) * 100) : 0);
}}
>
<Stack gap="s" padding="s">
<Text element="p">The onScroll handler is called after the fade state has updated.</Text>
<Text element="p">Use it to track scroll progress or build scroll-linked behaviour.</Text>
<Text element="p">The ref gives you the scroll container element itself.</Text>
<Text element="p">Keep scrolling to reach the bottom.</Text>
</Stack>
</Scrollable>
</Stack>
);
}
Accessibility
- When you provide an
aria-labeloraria-labelledby, the container is exposed as aregionlandmark and becomes keyboard focusable, so keyboard users can tab to it and scroll with the arrow keys. - Without an accessible name the container is a plain
div— appropriate when the scroll region isn't a meaningful standalone area, or when an interactive child already provides keyboard access to the content. - The edge fade is purely decorative and doesn't hide content from assistive technology.