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×

0.5× background — falls behind
the page itself — reference
1.5× foreground — pulls ahead

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.