3D & MotionJul 14, 20269 min readby Alex Carter

Designing depth: scroll-driven 3D that feels cinematic, not gimmicky

Most "3D websites" feel like tech demos — impressive for ten seconds, exhausting after a minute. The good ones feel like films. This is the system behind sites like this one: the rules, the math and the discipline that keep depth on the side of cinema.

Translucent glass panels floating at different depths

Every few months a website goes viral for "feeling like a movie", and every few months a hundred clones appear that feel like unlicensed bumper-car rides. The difference is never the technology — it's the same CSS perspective and translateZ in both cases. The difference is that the film-like sites treat depth as a storytelling device with rules, while the clones treat it as a party trick. After eight years and 120+ projects, here is the rulebook I wish someone had handed me in 2018.

Rule 1 — Set a Z-layer budget and defend it

Depth costs attention. Every layer you place between the visitor and the content is a question the visitor's brain must answer: is this important, or is it decoration? Ask too often and the visitor stops answering — they just feel tired and call your site "heavy" without knowing why.

So before writing a single transform, I assign every section a depth budget on paper. A typical page gets three depth tiers, never more:

  • Foreground tier (translateZ 0 to 200px): actual content — titles, cards, CTAs. This is where the visitor's focus lives.
  • Atmosphere tier (translateZ −300 to −800px): dimmed context — background words, ghost images, soft particles. Support, never lead.
  • Camera tier (the scroll itself): the movement that travels between tiers. One camera, one direction, deliberate.

If a design asks for a fourth tier, it is really asking for a second page. Split it.

Rule 2 — One camera, and it only moves on scroll

Cinema has one camera at a time. Websites break this constantly: the hero dollies forward while a parallax side-column drifts up while a canvas starfield warps — three cameras, three exposure languages, one seasick visitor.

The fix is boring and brutal: pick one scroll-driven camera move per viewport. On this very page, the corridor section is the camera; everything else on screen stays still while it plays. When the sequence ends, the camera "rests" and a new sequence may begin further down. Sequences, not a continuous swirl — that is what makes it feel edited rather than generated.

Depth is not how much moves. Depth is how clearly things arrive from a place.
— Studio rule n°3, taped above my monitor since 2019

Rule 3 — Scrub with progress, not with events

Here's the math mistake I see everywhere. People attach depth effects to scroll events ("every 100px, move the card"), which makes the animation depend on the speed of the wheel. Fast scroll = teleporting cards; slow scroll = dead page. The visitor's hardware ends up directing your film.

The fix: express every effect as a function of progress, not pixels. For an element entering the viewport, progress is:

p = (viewportHeight - rect.top) / (viewportHeight + rect.height)
// p = 0 → element below the fold
// p = 1 → element fully passed
// clamp(p) → drive translateZ, opacity, blur

Now the animation is anchored to the position of the element in the viewport, not the speed of the wheel. A card that "flies in from 700px deep" does so at exactly the same place for a trackpad flick and a careful drag. That consistency is what the eye reads as physical space rather than animation.

Performance note: write the progress into a CSS custom property (--dp) and let the transform live in CSS: translateZ(calc((1 - var(--dp)) * -700px)). One rAF loop updates a number; the browser's style engine does the rest. No per-frame string concatenation on hundreds of elements.

Rule 4 — Blur and fade are your depth-of-field

Real cameras don't render everything in focus, and neither should you. A card flying in from translateZ(−700px) should arrive with a slight blur that resolves to sharp exactly when it "lands" near the camera. That resolve — from soft to crisp — is the single strongest depth cue you can add, and it costs one filter: blur() interpolated by the same progress value.

The fade matters just as much. Elements deep in the scene should be dimmer than elements near the camera, the way fog eats distant buildings. If everything is full-opacity, the visitor's brain concludes it's looking at stickers on glass, not a space.

Rule 5 — Give every sequence a landing

The most common failure of scroll-driven 3D isn't the entrance — it's the aftermath. The element flies in beautifully… and then sits mid-air, halfway transformed, drifting at some arbitrary progress value because the math never quite reaches 1. Content that never lands can't be read calmly.

Design the landing explicitly: choose the scroll position where progress hits exactly 1.0 and make sure that position is "content comfortably in the middle of the screen". Everything before is theatre; from that point on, the element is a normal citizen of the layout. The visitor gets a show and a readable page — which is the entire contract of cinematic web design.

Shipping checklist

  1. Depth budget written down per section — three tiers maximum.
  2. One camera move per viewport, sequences clearly separated.
  3. All effects functions of progress, updated in a single rAF loop.
  4. Depth-of-field via blur + opacity, resolved at the landing point.
  5. Landing position chosen on purpose and verified on mobile.
  6. prefers-reduced-motion gets the same content, minus the ride.

Follow the six items above and your site will feel directed rather than decorated. Ignore them and no amount of GPU power will save it. Depth is a discipline long before it is an effect — which is exactly why it still turns heads.

← Previous From Figma to browser: shipping award-worthy interactions