Scroll area
A native scroller with a 4px overlay scrollbar and edge fades that grow with hidden content.
import { ScrollArea, useStickToBottom, useScrollKeys and 3 more } 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, the chrome the top fade starts under and the overlay scrollbar's size and inset.
--scroll-fade-size- TokenRef<Space>Default var(--space-6)
- The longest edge fade (24).
--scrollbar-size- TokenRef<Measure>Default var(--scrollbar-size-visible)
- The overlay scrollbar's thickness.
--scrollbar-inset- TokenRef<Space>Default var(--space-0-5)
- The scrollbar's distance from the edges.
--scroll-radius- TokenRef<Radius> | Derived | "0px"Default 0px
- The corner of the box it sits in: the bar's cap stays concentric with it (R4). Default 0px.
--scroll-fade-inset-top- TokenRef<Measure> | TokenRef<Control> | Derived | "0px"Default 0px
- Chrome over the top edge: the top fade starts below it. Default 0px.
--scroll-fade-inset-bottom- TokenRef<Measure> | TokenRef<Control> | Derived | "0px"Default 0px
- The same on the bottom edge (a floating button). Default 0px.
--scroll-fade-inset-alpha- "0" | "1"Default 1
- Past an inset: 1 paints (a sticky label inside), 0 hides rows under see-through chrome. Default 1.
--scroll-padding-block- TokenRef<Space> | "0px"Default 0px
- A row scrolled into view stops this far from the edges. Default 0px.
--scroll-padding-top- TokenRef<Measure> | Derived | "0px"Default var(--scroll-padding-block)
- The same at the top alone. Default --scroll-padding-block.
--scrollbar-offset-top- TokenRef<Measure> | Derived | "0px"Default 0px
- Chrome over the top edge: the vertical bar starts this much lower. 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" | falseDefault false
- 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- booleanDefault true
- false:
overflow-anchor: none, for scrollers whose pinninguseStickToBottomowns (the thread). stickyBar- booleanDefault false
- Horizontal: the bar sticks to the bottom of the scroller around it while the area shows (a long diff).
timeline- `--${string}`
- Names the viewport's block scroll timeline, for chrome that follows the scroll.
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- numberDefault 26
- 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.