Scroll area
A native scroller with a 4px overlay scrollbar and edge fades that grow with hidden content.
import { ScrollArea, useStickToBottom, useScrollKeys, … } from "@arcadia/ui"Size the root, with a height or a max-height, and the viewport fills it. Scrolling is native. The 4px steel scrollbar overlays the content, so it takes no width; it shows while the area is hovered or scrolling and fades 600ms after. The edge fades grow with the hidden distance, from 0 to 24px, so a list scrolled to the top has no fade there. Scroll the list above to watch both ends change.
A transcript passes viewportRef and contentRef to useStickToBottom and sets anchoring={false}, so the hook owns pinning to the newest message.
Scrolls the app starts
Reader scrolling is never smoothed or scripted.
springScroll(el, to) is a critically damped spring (ω 26, ζ 1): half-way in about 65ms, settled in about 300ms, with no overshoot. It re-aims every frame when to is a function, so a jump lands on a bottom that keeps growing; beyond 1.5 viewports it fast-forwards; and it stops the moment the reader touches anything, while a trackpad's momentum tail from before it started neither stops it nor fights it. useStickToBottom's jumpToBottom() runs on it and lands pinned. Under reduced motion it is one instant write.
useScrollKeys(ref) puts PageUp, PageDown, Home, End and Space on the spring while focus is in the scroller (or on the page, with whenBodyFocused); a press while it runs steps from its target, so holding PageDown never falls behind. revealInView(viewport, row) brings a row in by its nearest edge with two rows of margin, and does nothing when it is already in view.
Only a scroll the app starts moves on a spring: a jump to the latest message, PageUp, PageDown, Home and End in a scroller, revealing the active row.
Examples
rootStyle takes where the scroller sits, the longest edge fade and the overlay scrollbar's size and inset.
| Knob | Takes | Default | Sets |
|---|---|---|---|
TokenRef<Space> | var(--space-6) | The longest edge fade (24). | |
TokenRef<Measure> | var(--scrollbar-size-visible) | The overlay scrollbar's thickness. | |
TokenRef<Space> | var(--space-0-5) | The scrollbar's distance from the edges. | |
TokenRef<Radius> | Derived | "0px" | 0px | The corner of the box the scroller sits in: the bar's straight run ends r − 2 from the outer edge, so its cap is concentric with the corner (R4). Default 0px. |
The viewport is a normal scroller: wheel, touch and keyboard scrolling all work, and keyboard focus draws an inset ring on the root. Pass viewportProps for a role or a label, such as role="log" on a transcript, so the region is announced for what it is.
- ↓↑
- Scroll a line
- PageDownPageUp
- Scroll a page
- HomeEnd
- Jump to the start or the end
ScrollArea
fade- "y" | "x" | "both" | "top" | "bottom" | "left" | "right" | falseDefaultfalse
- Mask the edges that have hidden content; the fade grows with the hidden distance (0 → 24 px).
orientation- "vertical" | "horizontal" | "both"Default"vertical"
- Which axes scroll (and get an overlay scrollbar).
anchoring- booleanDefaulttrue
- false:
overflow-anchor: none, for scrollers whose pinninguseStickToBottomowns (the thread). stickyBar- booleanDefaultfalse
- Horizontal: the scrollbar sticks to the bottom of the scroller the area sits in while the area is on screen, so a tall sideways scroller (a long diff) can be scrolled without reaching its end.
viewportRef- Ref<HTMLDivElement>
- The scrolling element (pass it to
useStickToBottom). contentRef- Ref<HTMLDivElement>
- The content wrapper (the second
useStickToBottomref). viewportProps- Omit<ComponentProps<"div">, "ref" | "children">
- Extra props for the viewport, e.g.
role="log",aria-label. rootStyle- RootStyle<ScrollAreaKnobs>
- Typed StyleX override: where the scroller sits (layout) and its knobs.
className- string
- A product's hook class for its own rules (being retired: new code sets
rootStyle).
Rest props go to <div> props.
omega- numberDefault26
- Natural frequency in rad/s.
onFrame- (top: number) => void
- After every write, with the new
scrollTop. onEnd- (reason: SpringScrollEnd) => void
- Once, with why it stopped.
whenBodyFocused- boolean
- Also take the keys while nothing has focus (
<body>): the app's main scroller. end- () => void
- End jumps to the bottom through this (a transcript's
jumpToBottom(true), which re-pins). onKey- () => void
- Called before a key moves the scroller, so a stick-to-bottom hook reads it as the reader's.