ScrollView
A scroll area with bars you style or paint, kept moving by the compositor while the page is busy.
SourcePackage @kerf-js/veneer
Preview
tsx
import { ScrollView } from '@kerf-js/veneer'export function Rows() { return ( <ScrollView label="Build log" axes="y" style="height:220px" > {builds.map((n) => ( <p style="margin:0;padding:8px 12px"> Build {n} passed </p> ))} </ScrollView> )}const builds = Array.from({ length: 60 }, (_, i) => i + 1)Installation
npm install @kerf-js/veneerUsage
tsx
import { ScrollView } from '@kerf-js/veneer'Fixed thumb
thumb={{ size: 28 }} keeps the thumb one size however long the content. bar.show and bar.layout keep it in view, in a gutter of its own.
tsx
import { ScrollView } from '@kerf-js/veneer'export function FixedThumb() { return ( <ScrollView label="Events" axes="y" thumb={{ size: 28 }} bar={{ show: 'always', layout: 'gutter', width: 8, hoverWidth: 8, radius: 2, }} style="height:220px" > {rows.map((n) => ( <p style="margin:0;padding:6px 12px">Event {n}</p> ))} </ScrollView> )}const rows = Array.from({ length: 400 }, (_, i) => i)Painted on a canvas
renderer={canvas({ draw })} from @kerf-js/scroll/canvas paints each bar; paint is the default look to draw over.
tsx
import { canvas } from '@kerf-js/scroll/canvas'import { ScrollView } from '@kerf-js/veneer'export function Canvas() { return ( <ScrollView label="Lines" axes="y" renderer={notched} bar={{ width: 8, hoverWidth: 12 }} style="height:220px" > {lines.map((n) => ( <p style="margin:0;padding:6px 12px">Line {n}</p> ))} </ScrollView> )}const lines = Array.from({ length: 80 }, (_, i) => i)const notched = canvas({ draw(ctx, frame, paint) { paint(ctx, frame) const { x, y, width, height } = frame.thumb ctx.fillStyle = 'rgb(255 255 255 / 0.85)' ctx.fillRect(x, y + height / 2 - 1, width, 2) },})Snap points
snap rests on each [data-slide] once scrolling stops, as CSS scroll snapping does. An arrow key moves one slide.
tsx
import { ScrollView } from '@kerf-js/veneer'export function Snap() { return ( <ScrollView label="Slides" axes="x" snap={{ selector: '[data-slide]', type: 'mandatory', axis: 'x', }} style="height:170px;max-width:520px" > <div style="display:flex;gap:12px;width:max-content"> {slides.map((s) => ( <div data-slide="" style={slide} > {s} </div> ))} </div> </ScrollView> )}const slides = ['One', 'Two', 'Three', 'Four', 'Five']const slide = 'flex:0 0 240px;height:140px;display:grid;' + 'place-items:center;border-radius:12px;' + 'background:var(--veneer-color-surface-sunken)'When to use
Use it for
- A panel, list or log whose scrollbar must look the same on every platform
- A thumb of a fixed size, bars in your own colours, or bars painted on a canvas
- Snap points, or (with `mode="engine"`) your own easing and momentum
Reach for something else
- ScrollRail, when it is the whole page that scrolls
- VirtualList, when it is a long list of rows: it brings its own scroller
Accessibility
- Arrowkeys
- Scroll by a line
- PageUporPageDown
- Scroll by most of a screen
- SpaceorShift+Space
- Scroll down or up by most of a screen
- HomeorEnd
- Go to the top or the bottom
- The content stays in the DOM and in the accessibility tree; the viewport keeps real scroll offsets, so focus, find-in-page and screen readers reach every part of it.
- With
label, the viewport is a namedregion. It is a tab stop only when nothing inside it can take focus. - Each bar is a
scrollbarwitharia-valuenowandaria-controls. - Under reduced motion, animated scrolls are instant and the bars stop fading.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
children | unknown | — | What it contains. |
controller | ((s: Scroller) => void) | — | Receives the scroller: scrollTo, scrollIntoView, state and the rest. |
mode | "native" | "engine" | — | Who moves the content. native (default): the browser scrolls it, on the compositor, so it keeps moving while the main thread is busy; this draws the bars and runs the API, and the wheel, touch and key options are the browser's own. engine: every input runs here, with this package's easing, momentum, rubber band and keys. |
axes | Axis | "both" | 'both' | y, x or both (default). An axis whose content fits shows no bar. |
thumb | ThumbOptions | — | size for a fixed thumb; else min and max bound a proportional one. |
bar | BarOptions | — | Width, hover width, inset, radius, colours, show, layout, fade and track-press behaviour. |
renderer | Renderer | — | Draws the bars. Default: DOM elements styled by CSS. canvas() paints them. |
duration | number | — | Animation length for wheel notches, keys and smooth scrollTo, ms. Default 220. |
easing | Easing | — | |
friction | number | — | Touch inertia: the share of velocity kept per ms. Default 0.998 (iOS's normal rate). |
momentum | boolean | false | Coast after a touch fling. Default true. |
wheelSpeed | number | — | Wheel distance multiplier. Default 1. |
lineHeight | number | — | px for a wheel "line" and an arrow key. Default 40. |
overscroll | "none" | "bounce" | — | Rubber-band past the edges on touch. Default bounce. |
chain | "auto" | "contain" | — | At an edge, hand the gesture to the scroller outside (auto), or keep it (contain). Default auto. |
snap | SnapOptions | — | |
keyboard | boolean | false | Arrows, Page keys, Home, End and Space scroll the area. Default true. |
autoscroll | boolean | false | Scroll while a text selection or a drag is held past the edge. Default true. |
touch | "native" | "js" | — | Touch input: js (default) runs it here; native leaves touch to the browser. |
grab | boolean | false | Drag the content with a mouse, as a hand tool. Default false. |
motion | "auto" | "full" | "reduce" | — | reduce makes every animated scroll instant. Default auto: follows prefers-reduced-motion. |
label | string | — | The accessible name of the viewport, which makes it a region. |
syncEvents | boolean | false | Also fire a scroll event in the frame a position is written, so listeners draw with that frame. Default true. |
| Event | Type | Default | Description |
|---|---|---|---|
onScroll | ((state: ScrollState) => void) | — | Called once per frame in which the position or overscroll changed. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="scroll-view"] [data-part="viewport"]
viewportcontentscrollbarthumb
RelatedScroll railVirtual list