Input · 13
Virtual scroll
虚拟滚动 · virtual-scrollCombine wheel, drag and keys into one bounded progress value, then smooth its display.
A horizontal story may use the vertical wheel to move sideways. Wheel deltas can mean pixels, lines or pages, so raw numbers behave differently across devices. At either end, let outward wheel input scroll the page again.
Formula
deltaPx = wheelPixels(event) if shouldCapture(deltaPx): preventDefault; push(deltaPx) current ← damp(current, target, λ, dt) progress = (current − min)/(max − min)
Parameters
| This page | Mature libraries | |
|---|---|---|
| deltaMode | 0 / 1 / 2 | Lenis converts pixels, lines and pages |
| λ | 10 /s | Lenis lerp 0.1 |
Doing it well
- Convert wheel
deltaModebefore updating one shared target. Lenis uses a line height or viewport size for line and page events.lenis packages/core/src/virtual-scroll.ts:4–10,142–153 - Clamp the target immediately at the bounds, then damp the displayed value using dt. Input and display remain distinct.lenis packages/core/src/lenis.ts:115; animate.ts:43
- At an edge, release only outward wheel movement to the page; inward movement still belongs to the component.
In our cases
primitives.json has no separate virtual-scroll row; a wheel-model audit classifies marathon and boutique, among others, as continuous input. marathon pushes wheel and touch movement into one target, the pattern unified here.
Used in 18 cases
Reference implementation · basics.js
/** Wheel deltas arrive in pixels, lines or pages depending on device. Normalise to pixels. */
export const wheelPixels = e => e.deltaY * (e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? innerHeight : 1);
/**
* A section that owns its own scroll position: inputs move `target`, `current` follows with damp.
* shouldCapture(): hand the wheel back to the page at the ends instead of trapping the user.
*/
export class VirtualScroll {
constructor({ min = 0, max = 1000, lambda = 10, multiplier = 1 } = {}) {
Object.assign(this, { min, max, lambda, multiplier }); this.current = this.target = min;
}
shouldCapture(delta) {
return !((delta < 0 && this.target <= this.min) || (delta > 0 && this.target >= this.max));
}
push(delta) { this.target = clamp(this.target + delta * this.multiplier, this.min, this.max); }
step(dt) { this.current = damp(this.current, this.target, this.lambda, dt); return this.current; }
get progress() { return normalize(this.current, this.min, this.max); }
}