Shots in this scene
Sticky Storytelling and Smooth Scrolling
In one line
Scrollytelling pins a stage with position: sticky while steps scroll past it, and an observer says which step is current. Smooth-scroll libraries such as Lenis replace the browser's scrolling with their own, which is why they are controversial.
Think of it like this
A museum guide with a lantern. The painting stays on the wall (the stage), and as you walk past the labels one by one, the guide changes what the lantern lights up. (Data Viz Topic 17.3 built this with a chart as the stage; here is the animation side.)
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- scrollytelling
- A page where a stage stays in place and changes as steps of text scroll past.
- position: sticky
- An element that scrolls with the page until it reaches an offset (like
top: 0), then stays there while its parent is on screen. - smooth scrolling library
- A script (such as Lenis) that moves the scroll position in eased steps instead of leaving it to the browser.
- scroll-jacking
- Taking control of scrolling away from the user: blocking the wheel, locking the page, or changing its speed so it no longer feels native.
Step by step
01Find the current step
In "A stage that changes with the steps" the stage says Battery, Motor, Frame as each step crosses the middle.
02Check the keyboard
Click the box and press Down or Space: it scrolls, because it is a normal scroller with focus.
03Run Lenis
"Smooth scrolling with Lenis" scrolls to 200 px in 400 ms. With Reduced motion on, it jumps at once.
04Clean up
destroy() removes the lenis class. Call it when a component goes away.
Try it yourself
- 1
Move the line
In the first example change the observer's
rootMarginto"0px 0px -90% 0px". The measured result isstep 2: the stage says Batteryandstep 3: the stage says Motor: the stage is one step behind, because the line now sits near the top, and the step you have scrolled to has not reached it.
Code & diagrams
A sticky stage in a 140 px scroller, and an observer with a line across the middle. The script scrolls for you.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
at load the stage says: Battery
step 1: the stage says Battery
step 2: the stage says Motor
step 3: the stage says Frame
the scroller can be focused and scrolled with the keyboard: trueLenis on a box. Run it with the timeline's Reduced motion switch on: the second line becomes false, because the scroll is instant.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
Lenis 1.3.27 · class on the wrapper: scroller lenis
part way after 150 ms of a 400 ms scroll: true
arrived: scrollTop 200, still scrolling: false
after destroy() the class is gone: scrollerBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Give the stage a parent that is only as tall as the stage
In "A stage that changes with the steps" wrap the stage in a div: <div><div class="stage" id="stage">Battery</div></div>.
Myth vs fact
Myth
Smooth scrolling libraries are needed for scroll animations.
Fact
Scroll-driven animations work on native scrolling. A library only changes how the scroll position moves. Most sites in this phase need none.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A smoothed scroll position lags the input, so scroll-driven animations look a little late. Lenis leaves touch native by default (
syncTouchisfalse).
Remember this
- 1
The problem this solves: a long explanation (battery, motor, frame) is easier when one picture stays on screen while the words change. You need a way to hold the stage in place and a way to know which step the reader is on.
- 2
**
position: stickyholds the stage.** A sticky element scrolls normally until it reaches its offset (top: 0), then stays, but only while its parent is still on screen. In the first example the stage sticks to the top of the box (measured: after 200 px of scrolling it is at the box's top edge). Wrap it in a parent only as tall as itself and it scrolls away (measured: -199 px). - 3
An observer picks the step. An
IntersectionObserverwithrootMargin: "-50% 0px -50% 0px"shrinks the observed area to a line across the middle of the scroller. The step crossing it sets the stage's text (measured: Battery, Motor, Frame). A bottom margin of-90%puts the line near the top, and the stage changes one step later. - 4
Make it work for everyone. Every step is real text in reading order, so a screen reader reads the story without the stage. Keep
aria-liveoff on the stage. A scroller that is not the page needstabindex="0"and a name (role="region",aria-label) so keyboard users can focus and scroll it (the first example printstrue). Find-in-page and#anchorlinks work because the scrolling is native. - 5
Smooth-scroll libraries. Lenis (1.3.27 here) keeps the real
scrollTopbut moves it in eased steps on its ownrequestAnimationFrameloop, so scroll-driven animations and observers still follow it. The second example adds the classlenisto a box,scrollTo(200)takes 400 ms (part-way after 150 ms), anddestroy()removes the class. - 6
Lenis needs its loop, and respects reduced motion. With
autoRaf: trueit runs its own loop; without it you must calllenis.raf(time), or nothing moves (measured:scrollTo(200)leavesscrollTopat 0,isScrollingstuck at"smooth"). In 1.3.27respectReducedMotionis on by default: with the Reduced motion switch the samescrollTojumps at once (measured). - 7
The scroll-jacking debate. Against: smoothing changes a speed people have learned, can be uncomfortable with vestibular conditions, and runs JavaScript every frame (battery). For: a consistent feel and an eased
scrollTofor anchors. Rules: never block the wheel or lock the page, keep touch native (Lenis does by default), turn smoothing off for reduced motion and the site toggle.
Explain it without notes
How does the first example decide which step is current, and why does it still work with a keyboard and find-in-page?
Give two reasons to be careful with a smooth-scroll library, and one way to make it safer.
Practice
Write the CSS and the three HTML parts of a sticky stage with two steps, so that the stage sticks to the top of the page.
Make Lenis run only when motion is allowed, and stop when the visitor switches motion off.
Done when you can
You can build a sticky stage and say why its parent must be tall
You pick the current step with an observer and keep native scrolling for the keyboard
You know what Lenis does, and that it needs
autoRafanddestroy()You can argue the scroll-jacking trade-off and switch smoothing off