Pattern 02 — transform + requestAnimationFrame
Depth is a speed difference
Nothing here has a z-coordinate. The glow lags, the rings keep pace, the dots overtake — and your visual system does the rest. Scroll and watch the layers separate.
The recipe
Parallax earned its bad reputation from implementations that fight the browser:
animating top or background-position (layout and paint
on every frame), or doing arithmetic directly inside the scroll event (which can
fire far more often than the display refreshes). The fix is a pair of habits:
Move only transforms. translate3d is applied at the
compositing stage — no layout, no repaint, and the work rides the GPU.
Batch with requestAnimationFrame. The scroll listener merely
raises a flag; one rAF callback does all measuring and writing, once per painted
frame, reads before writes. The listener is passive so the browser
never waits on it to start scrolling.
let scheduled = false;
window.addEventListener('scroll', () => {
if (scheduled) return; // one update per painted frame, max
scheduled = true;
requestAnimationFrame(applyParallax);
}, { passive: true }); // never blocks the scroll itself
Demo 02 — Six shapes, five speeds
data-speed: 0.4 · 0.55 · 0.7 · 1.25 · 1.5 · 1.8 — slower reads far, faster reads near
The displacement formula: shift = distanceFromViewportCenter * (speed - 1).
At speed 1 the shift is zero — the element is just part of the page again.
Demo 03 — The same card at 0.5×, 1×, 1.5×
Keep scrolling through this tall section: all three start aligned, then shear apart. Subtlety wins — production parallax rarely needs more range than 0.8 to 1.2.
The background-attachment: fixed trap
Why the classic one-liner fails on mobile
background-attachment: fixed looks like free parallax, but it
forces the browser to repaint the background against every scroll position —
the image cannot live on its own compositor layer. On desktop you pay in
dropped frames on large images; on iOS Safari the value is effectively
ignored (the background scrolls with the page or misrenders), and Android
browsers have shipped years of assorted glitches with it.
If you want a fixed-feeling backdrop, put the image on its own element with
position: fixed (or transform it at speed 0 with a driver like
the one on this page) and let the content scroll over it. Same effect,
compositor-friendly, and it degrades to "just a picture" instead of to a bug.
Restraint clause
Parallax is seasoning, not the meal. One hero, maybe one interlude — after that,
depth cues stop informing and start performing. And because vestibular disorders
make moving backgrounds genuinely unpleasant, the driver below checks
prefers-reduced-motion and simply never moves a layer for those
readers: the page remains a perfectly good static composition.