CaissonBlog

Smooth wheel scrolling with one spring, and what to leave alone

A scroll story needs a mouse wheel that glides instead of jumping 100 pixels at a time. Caisson Studio's wheel.js chases a target with a critically damped spring, leaves every other input native, and replaced a library.

Published
3 min
To read
Studio
Project

A mouse wheel does not send a smooth stream of movement. Each notch is a jump of 100 pixels or so, and on a page where the scene reshapes as you scroll, a jump means the scene lurches. Pages with a scroll story usually fix that with a smooth scrolling library. Caisson Studio replaced the library it was using with a small file of its own, wheel.js, and the way it works is a good example of how much can be left out.

One target and a spring

When a wheel notch arrives, the script does not scroll the page. It moves a target position by the size of the notch. A critically damped spring then pulls the page's real scroll position toward that target on every frame.

A critically damped spring is the fastest approach to a target that does not overshoot. The page accelerates toward the target, slows as it arrives, and stops on it without bouncing past. The motion curves into place instead of travelling in a straight line and stopping dead, which is the part that reads as smooth.

The spring has a stiffness, written omega, and the script uses two values:

Omega 9 for wheel clicks of 50 pixels or more
A notched wheel gives large, widely spaced jumps, and a softer spring turns each into a glide.
Omega 16 for trackpads and free-spinning wheels
These already send many small steps, so a stiffer spring follows them closely and the page feels attached to the finger.

A tick(dt) function advances the spring each animation frame, before the page reads its scroll position, so that everything drawn in that frame agrees on where the page is.

What it does not touch

The more important design decision is the list of things the script leaves native:

The keyboard,
including page up, page down and the arrow keys.
The scrollbar,
dragged by hand.
Touch scrolling,
which the operating system already smooths.
Anchor links
Any box with its own overflow
A scrolling panel inside the page keeps the browser's wheel handling, so a long table or a code block does not fight the page for the wheel.

For those inputs the script does nothing except listen for the scroll event, so that its target stays in step with where the browser actually put the page. It also does nothing at all when the visitor has asked for reduced motion.

Taking over only the wheel keeps the script's responsibilities small, and everything else about scrolling stays the browser's job.

Testing it with synthetic events

The check is a script that sends synthetic wheel events and measures what the page does:

  • Twelve clicks of 100 pixels moved the page by exactly 1,200 pixels. A smoothing layer must not lose or invent distance.
  • No frame in the train of movement had a near-zero speed. A page that stops for one frame between clicks shows up as a stutter, so the test looks for it.
  • The slowest speed during the train was 400 pixels per second.

Why not keep the library

The library worked. Replacing it made the pacing of the page predictable, because the spring constants, the list of inputs it handles and the order it runs in each frame are all in one short file that the rest of the page's code can be written around. The page was also one dependency lighter.

This script is used on caissonai.co and on the Cohentra site. The post on the Cohentra scroll story covers how that site reached 105 fps, and the Studio page has the rest of what the studio builds.

Written by Caisson AI. Questions or corrections go to hello@caissonai.co.