Four patterns that earn their place
Staggered entry. Elements fade and translate a short distance as they cross into the viewport. This is the workhorse. It costs almost nothing, it survives on any device, and it gives long pages a rhythm. The tuning that matters is distance and threshold: 12–24px of travel, triggered when roughly 15–20% of the element is visible. Larger travel distances start to feel like the page is assembling itself in front of you.
Parallax Drift. Background layers move at a fraction of scroll speed. It reads as depth only when the ratio is generous — a background at 0.5× against a foreground at 1× is obvious, while 0.9× is wasted computation. Keep parallax to decorative layers. Parallaxing a text block means the reader is chasing the line they are trying to read.
Pinned sections. A section sticks while its internal content advances through steps. Excellent for sequential explanation — three states of a product, a before-and-after, a process. Two rules keep it usable: the pin must release predictably, and every step must also exist as readable content for anyone who scrolls straight past. Pinning works well with position: sticky and no library at all for simple cases.
Scroll-linked progress. A bar, ring or counter tied to document position. Purely informational, very cheap, and one of the few scroll effects that improves comprehension rather than atmosphere.
Comparison
| Pattern | Implementation | Mobile behaviour | Failure mode |
|---|---|---|---|
| Staggered entry | Intersection Observer + CSS class | Identical, reduce travel | Content invisible if script fails |
| Parallax Drift | Scroll-driven animation or rAF transform | Halve amplitude | Jank on long pages, motion sickness |
| Pinned sections | Sticky positioning or scroll timeline | Often better unpinned | Scroll hijack, trapped users |
| Progress indicator | Scroll offset ratio | Identical | Inaccurate on dynamic height |
Intersection Observer versus scroll-driven animations
For anything that happens once — reveal, count-up, lazy image swap — Intersection Observer is the correct tool. It is event-based, it does not run on every frame, and it works without measuring scroll position. A single observer with a sensible rootMargin can drive an entire page of reveals.
For anything that must track scroll continuously, CSS scroll-driven animations are the modern approach: the animation is declared in CSS and bound to a scroll or view timeline, so the browser can run it off the main thread. Support is still uneven across engines, which means it should be treated as an enhancement layered over a static fallback rather than the only path. Check current support on Can I use before committing a launch to it.
The pattern to avoid is a scroll event listener that reads layout on every frame. Reading getBoundingClientRect inside a scroll handler forces synchronous layout; do it for a dozen elements and you will drop frames on hardware that handles far more demanding pages fine. If you must use a listener, batch reads with requestAnimationFrame and cache anything that does not change between resizes.
Pros and cons of scroll-linked motion
In favour
- The user sets the pace, so the animation never becomes a wait.
- Reveals give long-form pages a sense of progress and structure.
- Depth cues can separate content layers without extra borders or shadows.
Against
- Heavy parallax is a common trigger for vestibular discomfort.
- Reveal-on-scroll can hide content from anyone who lands with JavaScript blocked.
- Pinned sections break the browser's own scroll expectations, including find-in-page.
Accessibility and fallback rules
Three rules cover most of the risk. First, opt in to hiding: apply the pre-reveal state only when a class such as has-js is present on the document element, so a script failure leaves everything visible. Second, respect the reduced-motion query by removing travel and keeping the fade, or by removing the animation entirely and showing the final state. Third, never move focus as a side effect of scrolling — a keyboard user tabbing through a pinned section should not be dragged around the timeline.
Anchor navigation deserves attention too. If a reveal is triggered by intersection and a visitor arrives at a deep anchor, elements above the anchor may never intersect and never reveal. Either reveal everything above the current scroll position on load, or accept that users who share links will see empty sections when they scroll up.
Tuning values worth writing down
Reveal travel of 16px, duration of 320ms, ease-out, and a 70ms stagger will look correct on the large majority of layouts. Parallax ratios between 0.3× and 0.6× are the usable band. Pinned sections need at least one viewport height of scroll per step or the transitions overlap. Everything beyond that is taste — which is exactly what the playground is for.