Feature Playground

Data-viz / scrollytelling · 12.1

Sticky-graphic scrollytelling

A museum exhibit where the placard moves and the artifact stays.

A graphic pins while step text scrolls past it, updating the graphic's state per step, then un-pins. The canonical data-storytelling pattern.

4 knobs

How it actually works

Every scrollytelling piece you have ever read is this. It is also the pattern most often built wrong, by attaching a scroll listener and recomputing every element's position on every scroll event. The Pudding rejected that in writing: scroll listeners "inhibit performance and are janky". That is the same conclusion our House Standard reached from measuring frames, arrived at from the other direction.

Plain CSS position: sticky holds the graphic. No JavaScript tracks dimensions, which is the Pudding's own emphasis. A light intersection trigger fires a callback only for state CHANGES: step 3 entered, step 2 re-entered. Scrollama, which the Pudding says is interchangeable with its own enter-view.js, is a thin wrapper over native IntersectionObserver that fires enter, exit, and 0 to 100% progress, deliberately decoupled from rendering. The offset decides where on screen a step "counts" — in raw IO that is a rootMargin, and turning the knob here rebuilds the observer.

The knobs, named

Step count, trigger offset, progress granularity, interpolation. The offset is drawn on the stage as a dashed line: drag the knob, watch the line move, watch the steps fire early or late against it.

KnobSourceWhat it teaches
Step count sourced How many boundaries the story has. Each step owns a target state for the graphic; scroll decides which one is current.
Trigger offset sourced Where in the frame a step counts as entered, as a fraction from the top. This is scrollama's offset and it is a rootMargin underneath.
Progress granularity sourced Step mode changes state only at boundaries. Progress mode also reports how far through the current step you are, and interpolates between the two states.
Interpolation ours How the graphic travels between two step states. Under step granularity this does nothing, which is itself the demonstration.

sourced means the source names this parameter. ours means the source names none and the knob is our design against the mechanism. No knob here is invented and passed off as sourced.

Evidence

VERIFIED (author, primary source)

pudding.cool/process/scrollytelling-sticky/ and /process/introducing-scrollama/. The offset, the progress events, and the choice of IntersectionObserver over scroll listeners are all sourced verbatim. The Pudding also documents the tradeoff map across six approaches; graph-scroll.js is the only one built specifically around this transition.

Seen on
pudding.cool. The awwwards form pairs scrollama with d3.js for the chart draw.
Dependencies
vanilla, with raw IntersectionObserver. scrollama.js if you want the ergonomics.
Difficulty
moderate — the CSS is trivial; the step-boundary logic and the mobile reflow are the cost
Performance
Chosen explicitly over scroll listeners, in the source's own words, because those "inhibit performance and are janky". One observer, one property write per step change.
Accessibility and the floor
Breaks entirely without JS unless you render a static first frame, so ours renders every step's text and the graphic at its final state. A chart of circles conveys nothing to a screen reader: the step text carries the argument here, and it is real text, always present, never revealed.
Where our build departs from the source: The sourced pairing is scrollama plus d3 for the chart transitions. Ours hand-rolls the interpolation, because d3 is heavy and this build imports nothing. For a marketing site with two or three charts that is the right call anyway; for a real newsroom piece with dozens, it is not.

Notes

Composability. The whole point is that the graphic is arbitrary. Put the funnel (12.2), the chart (12.3) or the odometer (12.4) in the sticky slot and the pattern does not change.

The honest split in our build: the step CHANGE is IntersectionObserver, exactly as sourced, and it fires only on a change. The within-step progress is read in the shared rAF read pass, because a fractional position is not something an observer can hand you without 100 thresholds. Scrollama makes the same trade behind its API. It is worth knowing that the two halves of that library are not the same mechanism.