VESYL UI

ScrollArea

A bounded scroll region that leaves the scrollbar to the platform and marks its edges with ScrollHint.

Nothing here styles the scrollbar, on purpose. Setting scrollbar-width or scrollbar-color makes Chrome opt out of macOS overlay scrollbars and draw a permanent bar that occupies layout width — the opposite of deferring to the OS. Left alone, the platform self-hides where it hides its own, keeps a bar where it keeps one, and brings momentum, click-to-page and thumb dragging with it.

The hints cover what that leaves — nothing shown at rest — and stand down when a scrollbar occupies layout space, so the two never say the same thing twice.

Basic

Jokester began sneaking into the castle in the middle of the night and leaving jokes all over the place: under the king's pillow, in his soup, even in the royal toilet.

The king was furious, but he couldn't help but laugh at the jokes. They were actually pretty funny. So, he declared that Jokester would be the official court jester.

And so, Jokester the Wise became the most beloved figure in the kingdom, known far and wide for his hilarious jokes and his ability to make even the grumpiest of kings smile.

The moral of the story? Never underestimate the power of a good laugh, and always be ready to find the humor in any situation.

Padding goes on the content, not the panel. className="p-4" pads the outside of the scroll region, so the scrollbar sits inside that padding and floats in from the panel’s edge. contentClassName="p-4" puts it inside the scroller, leaving the bar on the edge.

Scrolling list

Tags

React
Vue
Svelte
Angular
Solid
Qwik
Astro
Next.js
Nuxt
Remix
SvelteKit
Gatsby

Horizontal

Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8

Both directions

This content is wider and taller than the container, so you can scroll both horizontally and vertically to see all of it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Each axis is tracked separately — a region can overflow one way and fit the other, and hinting the axis that fits would point at content that is not there.

Props

PropType
axis'y' | 'x' | 'both'Which way it scrolls. Defaults to y.
bleednumberPull to the parent’s padding edge and re-apply that padding inside, so the scrollbar lands in the gutter rather than over content. Pass the parent’s own padding — p-6bleed={6}.
scrollHintbooleanMark edges with content past them. Defaults to true.
focusablebooleanTab-stop the viewport while it scrolls. Defaults to true. Pass false in popup lists that already arrow-navigate.
contentClassNamestringClasses for the padded wrapper inside the scroller — where padding belongs.
onViewportScrollUIEventHandler<HTMLDivElement>Called when the scrolling viewport moves; use for infinite loading and scroll-linked state.
classNamestringClasses for the outer region — height caps, borders, flex sizing.

A height cap on the region still bounds the scroller, through max-height: inherit; a percentage height against an auto-height parent would not.

Keyboard

The scroller takes tabIndex={0} only while it actually scrolls, unless focusable={false}. A region whose contents are not focusable would otherwise be unreachable without a pointer, and a region that fits would be a dead stop in the tab order. Select and Combobox popups pass false — their items already move with the arrow keys.

In a dialog

Use DialogBody — this component with the dialog’s padding already filled in. A hand-rolled scrolling div is what puts a scrollbar across a field’s right edge.