One gesture, one step: a scroll story that does not fight your hands

Our home page moves one chapter per swipe, wheel notch or key press. How the step lock tells a new gesture from trackpad inertia, and when it gets out of the way.

In short

  • The Commercium home page moves exactly one step per gesture: a wheel notch, a trackpad swipe, a key press or a touch swipe.
  • A small controller tells a new gesture from trackpad inertia using timing, size and direction, and ignores input while a step is animating.
  • With reduced motion switched on, or without JavaScript, the same content shows as calm stacked sections with every word and link.
  • The controller is about 375 lines of TypeScript on top of GSAP, with no scroll-jacking or smooth-scroll library.

The home page of this site tells our story in steps. The hero, a short statement, then one chapter for each app. Every gesture moves you exactly one step: one notch of a mouse wheel, one swipe on a trackpad, one press of an arrow key, one swipe on a phone. This post is about how that works, and about the times it deliberately does nothing.

The problem with scroll stories

A pinned, scroll-driven page looks lovely in a screen recording and often feels wrong in the hand. The usual failure is a trackpad. A single two-finger swipe on a Mac produces a burst of wheel events and then a long, fading tail of inertia events. If every event can advance the story, one swipe skips three chapters. If the page ignores events for a fixed time, a fast reader feels stuck.

Scroll-jacking libraries solve this by taking over scrolling completely. We did not want that. The page should scroll natively everywhere outside the pinned story, and the story should hand control back the moment you reach either end.

What counts as a new gesture

The controller listens to wheel, touch and key events. For the wheel, it first normalises the delta: a browser can report it in pixels, lines or pages, so a line becomes 40 px and a page becomes the viewport height.

Then it decides whether this event starts a new gesture or belongs to the one already moving. An event counts as new if:

  • more than 250 ms have passed since the last wheel event, or
  • the direction has changed, or
  • at least 320 ms have passed since the last step, the delta is at least 18 px, and it is at least 1.75 times the previous delta, or
  • the same timing and size hold and a short moving average of the last 10 deltas has risen above the long average of the last 70.

The last rule is the one that matters most on a trackpad. Inertia decays: each event is a little smaller than the one before, so the short average sinks below the long one. A real second swipe rises. Comparing the two catches a quick second swipe that arrives before the first one’s tail has finished.

const isNew =
  gap > NEW_GAP ||                       // 250 ms of silence
  dir !== this.lastDir ||                // a change of direction
  (sinceStep >= MIN_STEP_GAP && abs >= MIN_DELTA && abs >= RISE * this.lastAbs) ||
  (sinceStep >= MIN_STEP_GAP && abs >= MIN_DELTA && rising);

Swallow, then settle

Once a step starts, every further event is swallowed until it finishes. That single rule is what stops inertia from skipping a chapter. We test it with a simulated trackpad swipe of 73 wheel events with a long decaying tail: it moves the story exactly one stop.

On a desktop, the step itself is a scroll tween of 0.6 seconds with a gentle ease in and out, and the scene’s animation follows the scroll position. On a phone, scrolling is frozen while locked and the scene’s timeline is tweened instead, over 0.5 seconds. Phones get the quicker, simpler motion because a finger expects the page to answer at once.

Entering and leaving the story

Two edge cases decide whether a step lock feels trustworthy.

Entering. If you scroll towards a pinned section from outside it, a gesture that would carry you past its first stop lands exactly on that stop and locks. You never arrive halfway through a chapter.

Leaving. At the first or last stop, one more gesture in the same direction releases the lock and normal scrolling carries on. A large jump, such as dragging the scrollbar or pressing End, releases it at once. The controller remembers your last direction for 800 ms, so when it has to choose which end of a section to land on, it chooses the one you were heading for.

While locked, a small loop on each animation frame puts the scroll position back if momentum drifts it. On touch screens the page sets touch-action: none only while the story is locked, and restores it on release, so pinch zoom and normal panning work everywhere else.

When the answer is no motion

The best motion design includes a way out.

  • Reduced motion. If your device asks for reduced motion, the page never pins, never locks and runs no ambient loops. The chapters render as calm stacked sections with the same words, links and still images.
  • No JavaScript. The same stacked layout is what the server sends. Scripts only switch the motion layout on after they have started, and if the story has not taken over within five seconds, the page falls back to the stacked layout rather than leave anything hidden.
  • Pause. A Pause motion button stops every loop, scene and video on the page, in line with WCAG 2.2.2 (pause, stop, hide).

Keyboard users get the same steps as everyone else: the arrow keys, Page Up and Page Down, Space, Home and End. The dot rail on the side is labelled, marks the current chapter for screen readers, and each chapter has a link of its own, such as /#three-seals.

The budget

A scroll story is easy to make heavy. We set a budget before we started and test it on every build: the home page’s HTML, CSS and JavaScript must stay under 200 KB compressed, and its JavaScript under 90 KB. At launch the home page’s JavaScript is about 74 KB compressed, and around a third of that loads only as you approach the chapter that needs it. Only transforms and opacity are animated, blur is applied to one layer at a time and never to video, and the Three Seals scene pauses when it is off screen.

What we would tell anyone building one

Test with real inertia, not just single wheel events; a synthetic swipe with a long tail finds bugs a mouse never will. Decide what happens at the edges before you tune the middle. And build the stacked version first: if the story reads well with no motion at all, the motion is a bonus rather than a crutch.

The rest of the site follows the same rules. If you want to see the result, the home page is the whole demonstration.

Questions

What is a step lock on a web page?

A step lock turns continuous scrolling into discrete stops inside a pinned section. Each gesture moves to the next or previous stop, and at the first or last stop one more gesture in the same direction lets normal scrolling resume.

How do you tell a trackpad swipe from its inertia?

By treating a wheel event as a new gesture only if it comes after a pause of more than 250 ms, changes direction, or arrives at least 320 ms after the last step with a delta of 18 px or more that is at least 1.75 times the previous one. A short moving average rising above the long one also counts as a new swipe.

Does the site work with reduced motion or without JavaScript?

Yes. With reduced motion there is no pinning, no step lock and no ambient animation, and without JavaScript the same stacked layout is served. Every word and link is present in both.

More from the blog

All posts