Scroll Area
Contain scrolling content with slim scrollbars and fading edges that show more is available.
import { ScrollArea, ScrollBar } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use Scroll Area for a fixed-size region, such as a feed, build log or strip of items. Let main content scroll with the page. Avoid nested scrolling for long forms or content people must read in full.
import { ScrollArea } from '@youj/react'
export function Activity() {
return (
<ScrollArea aria-label="Activity" className="h-72">
<ActivityList />
</ScrollArea>
)
}Give it a height, or a width for horizontal scrolling. Scrolling is native: the wheel, touch, and the keyboard once the viewport has focus. The scrollbars overlay the content, appear while you hover or scroll, and fade away after.
Native scrollbars
ui.css gives the browser's own scrollbars the same thumb, on a transparent track. The page's bar is thin, and every other native bar takes the theme's colors, scopes included. Add yj-scrollbar to an element that scrolls natively, like a code panel or a long list, to make its bar thin and bring the thumb up to 3:1 contrast while the pointer or focus is on it.
<pre className="max-h-80 overflow-auto yj-scrollbar">{source}</pre>It sets only scrollbar-width and scrollbar-color, so bars that macOS overlays stay overlays, and hovering changes the color, never the layout. The thumb colors are the --ui-scrollbar-thumb, --ui-scrollbar-thumb-hover and --ui-scrollbar-thumb-active tokens. Set scrollbar-color: auto on an element to hand its bars back to the platform.
Description
A list of releases in a fixed-height Scroll Area, with slim scrollbars and fading edges.
Each edge with more to scroll fades out over 24px, by how far there is left to go on that side. At the start only the bottom edge fades. Scroll down and the top fade grows in, then the bottom one clears as you reach the end. The fades come straight from Base UI's overflow values, so they track the scroll exactly instead of switching on.
fade={false} turns them off, for content with edges of its own, like a table with a sticky header.
A strip that scrolls horizontally.
orientation="horizontal" renders a horizontal scrollbar and fades the side edges. Give the content w-max so it can grow past the viewport.
A region that scrolls on both axes.
orientation="both" renders both scrollbars and the corner where they meet. The two fades intersect, so each axis fades its own edges.
Code
scroll-area-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsorientation, fade, viewportClassName (3)
Props
| Prop | Type | |
|---|---|---|
orientation | 'vertical' | 'horizontal' | 'both'Which scrollbars to render. default 'vertical' | 'vertical' |
fade | booleanFade the edges that have more to scroll. default true | true |
viewportClassName | stringClasses for the viewport (the element that scrolls). | – |
Other props pass through: extends ScrollAreaPrimitive.Root.Props. | ||
ScrollBar, the bar and its thumb, is exported for custom compositions and takes Base UI's scrollbar props, such as orientation. ScrollArea already renders the bars its orientation needs.
Styling the viewportclassName styles the root, the element that has the size, border and…
Styling the viewport
className styles the root, the element that has the size, border and radius. viewportClassName reaches the element that actually scrolls, for padding that should scroll with the content or for scroll snapping.
<ScrollArea orientation="horizontal" viewportClassName="snap-x snap-mandatory" className="w-full">
<ul className="flex w-max gap-3 p-3">{slides}</ul>
</ScrollArea>MotionScrollbar in, on hover, scrollbar in, while scrolling, scrollbar out… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
Scrollbar in, on hoveropacity | duration.slowease.standard340ms · cubic-bezier(0.2, 0, 0, 1) | 340mscubic-bezier(0.2, 0, 0, 1) | |
| Scrollbar in, while scrollingappears almost at once | duration.instantease.standard80ms · cubic-bezier(0.2, 0, 0, 1) | 80mscubic-bezier(0.2, 0, 0, 1) | |
| Scrollbar outafter hovering or scrolling stops | duration.slowease.standard340ms · cubic-bezier(0.2, 0, 0, 1) | 340mscubic-bezier(0.2, 0, 0, 1) | |
| Thumbwidens from 6px to 8px and firms up on hover and press | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| Native thumb (yj-scrollbar)firms up while the pointer or focus is on the region | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| Edge fadesfollow the scroll position | no token tied to scroll, no timing | tied to scroll, no timing |
Reduced motion
Scrollbars appear and leave at once, and thumbs change at once. Scrolling and the edge fades don't change.
Reduced motion
The scrollbars appear and disappear at once, and thumbs change width and color at once. Scrolling itself, and the edge fades that follow it, are unchanged: they move only as you scroll.
AccessibilityKeyboard, ARIA, focus
Accessibility
- The viewport is a tab stop only when it can scroll, so a short list adds nothing to the tab order. Once it has focus, the arrow keys, Page Up, Page Down, Home and End scroll it.
- Name it with
aria-labeloraria-labelledby. The name goes on the viewport, which becomes aregion, so screen reader users can find it and hear what it holds. Without a name it adds no landmark. - The focus ring is drawn by the root, outside the viewport, so the edge fades can't hide it.
- The scrollbars are for the pointer. Keyboard and screen reader users scroll the viewport directly.