Parallax Scrolling in Pure CSS

Pure CSS parallax has two reliable methods. The perspective method makes a scroll container with perspective and pushes background layers back with translateZ, then scales them up by (perspective minus z) divided by perspective. The newer method ties a translate animation to animation-timeline: view(). Both should switch off under prefers-reduced-motion.

Three ways to get a parallax effect
MethodHow it movesWatch out for
perspective + translateZDeeper layers scroll slower inside a 3D scroll containerNeeds its own scroll container, not the page
animation-timeline: view()A keyframe translate follows the element through the viewportChrome and Edge 115+, Safari 26+, Firefox 161+
background-attachment: fixedBackground stays still while content moves over itPartial support on iOS Safari; lags on many phones
JavaScript fallbackScroll position sets a transform each frameUse requestAnimationFrame and a passive listener

Updated

Most parallax tutorials stop at background-attachment: fixed, which iPhones only partly support. This guide shows the two methods that actually animate layers, explains the scale maths so your layers do not shrink, and gives a small script fallback plus the reduced motion handling that keeps the effect from making anyone queasy.

Depth playground

.parallax { perspective: 1px; overflow-y: auto; overflow-x: hidden; }
.group { transform-style: preserve-3d; }
.back { transform: translateZ(-2px) scale(3); }

Method 1: perspective and translateZ

Two-layer hero in its own scroller

Scroll inside the frame. The striped layer sits at translateZ(-2px) and is scaled by 3, so it moves at a third of the scroll speed.

<style>
  body { margin: 0 }
  .parallax { height: 240px; overflow-x: hidden; overflow-y: auto; perspective: 1px }
  .group { position: relative; height: 240px; transform-style: preserve-3d }
  .back { position: absolute; inset: 0; transform: translateZ(-2px) scale(3);
          background: repeating-linear-gradient(45deg, #4F98FB 0 20px, #93c5fd 20px 40px) }
  .front { position: absolute; inset: 0; display: grid; place-items: center;
           font: 700 28px system-ui; color: #fff; text-shadow: 0 2px 6px rgb(0 0 0 / .4) }
  .plain { position: relative; height: 240px; background: #fff; padding: 16px; font: 16px system-ui }
  @media (prefers-reduced-motion: reduce) { .back { transform: none } }
</style>
<div class="parallax">
  <div class="group"><div class="back"></div><div class="front">Scroll me</div></div>
  <div class="plain">Normal content scrolls at full speed.</div>
</div>

Method 2: scroll-driven with view()

Image that drifts as its section crosses the screen

The animation only runs where animation-timeline is supported and the reader has not asked for reduced motion.

.hero { overflow: hidden; height: 60vh; }
.hero img { width: 100%; height: 130%; object-fit: cover; }

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .hero img {
      animation: drift linear both;
      animation-timeline: view(); /* after the shorthand */
    }
  }
}
@keyframes drift {
  from { transform: translateY(-15%); }
  to   { transform: translateY(0); }
}

JavaScript fallback

Small requestAnimationFrame parallax

Only used when scroll-driven animations are missing. The listener is passive and work happens once per frame.

const layers = document.querySelectorAll("[data-speed]");
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
const supported = CSS.supports("animation-timeline: view()");

if (!reduce && !supported) {
  let queued = false;
  const update = () => {
    for (const el of layers) {
      el.style.transform = `translateY(${window.scrollY * Number(el.dataset.speed)}px)`;
    }
    queued = false;
  };
  addEventListener("scroll", () => {
    if (!queued) { queued = true; requestAnimationFrame(update); }
  }, { passive: true });
}

How the perspective method works

The container becomes the scroller and gets a perspective value. Its children keep their 3D position with transform-style: preserve-3d. When the container scrolls, a layer sitting further back in z covers less screen distance than one at z equal to zero, so it appears to lag behind.

Because perspective on an element also creates a stacking context and a containing block for fixed children, keep the parallax wrapper self-contained. Headers that must stay fixed to the window belong outside it.

Phones and older browsers

Chrome's own write-up on performant parallax notes that momentum scrolling on iOS used to flatten the 3D context, which killed the perspective trick on iPhones. Test on a real device, and keep the content readable when layers do not move.

The scroll-driven method has no such problem because it runs on the page's normal scroller. Where it is unsupported, the keyframes never run and the layer stays in its resting position, which is a clean fallback.

How it works

  1. Move the depth slider in the playground to push the background layer further back.
  2. Scroll the preview and compare how fast the back layer moves against the front text.
  3. Read the calculated scale and speed, which are what the CSS below uses.
  4. Copy the method that fits: perspective for a contained hero, view() for sections on a normal page.

Tips

Frequently asked questions

Why does my translateZ layer look smaller?

Pushing an element away from the viewer with a negative translateZ shrinks it, exactly like a distant object. Add scale((p - z) / p) to restore its size. With perspective: 1px and translateZ(-2px), that is scale(3).

Does background-attachment: fixed work on mobile?

Not reliably. caniuse lists only partial support in iOS Safari and notes that most mobile devices are slow to update a fixed background after scrolling. Treat it as a desktop-only nicety and use one of the transform methods when the effect matters.

Is parallax bad for accessibility?

Large motion tied to scrolling can cause dizziness or nausea for people with vestibular disorders. Honour the operating system's reduce motion setting with the prefers-reduced-motion media query and show the layers static when it is on.

Which browsers support scroll-driven parallax?

As of October 2026, caniuse lists animation-timeline in Chrome and Edge from 115, Safari from 26 and Firefox from 161. Older browsers simply skip the animation, so the page still reads correctly without the effect.

Related: Scroll animations guide, prefers-reduced-motion guide, CSS transforms guide, CSS scroll snap, CSS animation generator.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap