Skip to content
kerfjsGitHub

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/veneer

Usage

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 named region. It is a tab stop only when nothing inside it can take focus.
  • Each bar is a scrollbar with aria-valuenow and aria-controls.
  • Under reduced motion, animated scrolls are instant and the bars stop fading.

API Reference

PropTypeDefaultDescription
childrenunknown—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.
axesAxis | "both"'both'y, x or both (default). An axis whose content fits shows no bar.
thumbThumbOptions—size for a fixed thumb; else min and max bound a proportional one.
barBarOptions—Width, hover width, inset, radius, colours, show, layout, fade and track-press behaviour.
rendererRenderer—Draws the bars. Default: DOM elements styled by CSS. canvas() paints them.
durationnumber—Animation length for wheel notches, keys and smooth scrollTo, ms. Default 220.
easingEasing—
frictionnumber—Touch inertia: the share of velocity kept per ms. Default 0.998 (iOS's normal rate).
momentumbooleanfalseCoast after a touch fling. Default true.
wheelSpeednumber—Wheel distance multiplier. Default 1.
lineHeightnumber—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.
snapSnapOptions—
keyboardbooleanfalseArrows, Page keys, Home, End and Space scroll the area. Default true.
autoscrollbooleanfalseScroll 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.
grabbooleanfalseDrag 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.
labelstring—The accessible name of the viewport, which makes it a region.
syncEventsbooleanfalseAlso fire a scroll event in the frame a position is written, so listeners draw with that frame. Default true.
EventTypeDefaultDescription
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