CSS Parallax Effects โ
3 Techniques for Adding Depth to Your Scroll
background-attachment, perspective transforms, scroll-driven animations. Three roads to the same visual trick โ but each with very different trade-offs. Let's break them down with live demos.

What Parallax Really Is โ And Why It Works
Look out a train window. Telephone poles flash by. Mountains barely move. The closer something is, the faster it seems to travel. That's parallax โ a depth illusion created by differential motion.
On the web, parallax means moving background and foreground layers at different speeds as the user scrolls. A flat page suddenly feels three-dimensional. It's a small trick with a big perceptual payoff.
But "parallax in CSS" isn't one technique โ it's at least three, and choosing the wrong one for the wrong context leads to janky animations, broken mobile layouts, and frustrated users. If you've ever shipped a parallax hero that stuttered on scroll or simply didn't work on iPhones, this guide is for you.
Technique 1: background-attachment: fixed โ The Simplest Entry Point
The easiest way to create a parallax-like effect is to fix a background image to the viewport while letting content scroll over it. The background isn't actually moving, but the relative speed difference between it and the scrolling content creates the illusion.
.parallax-section {
min-height: 400px;
background-image: url('hero.jpg');
background-attachment: fixed;
background-position: center;
background-size: cover;
}
The white content scrolls normally, but the gradient background stays pinned to the viewport. As you scroll, the background feels like a window โ a classic parallax illusion with just one CSS property.
This is the lowest-effort parallax you can build. One property, no JavaScript, instant effect. Perfect for section dividers with photographic backgrounds.
The catch? Mobile browsers (iOS Safari, Android Chrome) ignore background-attachment: fixed for performance reasons. On those devices, the background just scrolls normally. The page doesn't break โ but the parallax disappears.
If mobile support is non-negotiable, keep reading. The next two techniques work everywhere.
Back to content. Scroll further and you'll see the fixed background again โ a "peekaboo" effect between sections.
/* Blocks the main thread every frame. Jank city. */
window.addEventListener('scroll', () => {
el.style.backgroundPosition =
`center ${window.scrollY * 0.5}px`;
});
.parallax-section {
background-attachment: fixed;
/* Zero JS. Browser optimizes internally. */
}
Technique 2: perspective + translateZ โ Real 3D Parallax
CSS 3D transforms let the browser's rendering engine calculate actual depth. Set perspective on a scrolling container, then push child elements forward or backward along the Z-axis with translateZ(). Elements further away scroll slower โ just like looking out that train window.
.parallax-container {
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
perspective: 1px;
perspective-origin: center;
}
.layer--back {
transform: translateZ(-2px) scale(3);
/* Pushed back โ scrolls slower */
/* scale() compensates for the smaller apparent size */
}
.layer--front {
transform: translateZ(0);
/* Default depth โ scrolls at normal speed */
}
The scale() correction is key. When you push an element back with translateZ(-2px), it appears smaller (it's further from the "camera"). You compensate with scale(1 + abs(translateZ) / perspective) โ for perspective: 1px and translateZ: -2px, that's scale(3).
Foreground Content
This card sits at translateZ(0) โ the front layer. Scroll and watch the gradient background and mid-layer mountains move more slowly. That's real 3D parallax, calculated by the browser.
Technique 3: scroll-driven animations โ The Future Standard
Starting in 2024, CSS gained animation-timeline, a property that ties animation progress to scroll position instead of time. With it, you can create scroll-linked parallax in pure CSS โ no JavaScript, no hacks.
.parallax-bg {
animation: parallax-shift linear both;
animation-timeline: scroll();
animation-range: 0% 100%;
}
@keyframes parallax-shift {
from { transform: translateY(0); }
to { transform: translateY(-100px); }
}
animation-timeline: scroll() maps the animation's 0%โ100% to the scroll container's topโbottom. Assign different translateY distances to different layers, and each scrolls at its own speed. Parallax, in five lines of CSS.
Scroll-driven animations bind animation progress to scroll position. In this demo, the background text shifts upward, shrinks, and fades as you scroll โ all in pure CSS.
Unlike JavaScript scroll listeners, this runs on the compositor thread. You get guaranteed 60fps without blocking the main thread. No requestAnimationFrame, no throttle, no debounce.
The biggest advantage is zero JavaScript. The biggest caveat is browser support: Chrome and Edge have full support, Firefox has partial support, and Safari is still catching up. Progressive enhancement is essential.
Keep scrolling to watch the effect play out.
โ Scroll
Choosing the Right Technique
background-attachment: fixed is your go-to when you need a quick background parallax on desktop and can accept no effect on mobile. Minimum code, maximum simplicity. perspective + translateZ is the choice when you want physically accurate 3D parallax in a contained section โ hero areas, landing page headers. It works across browsers but demands careful layout management. scroll-driven animations is the future. Per-element speed control, compositor-thread performance, zero JavaScript. But browser support is still growing, so treat it as a progressive enhancement.
/* Base: no parallax (works everywhere) */
.hero-bg {
background: url('bg.jpg') center / cover;
}
/* Enhanced: scroll-driven parallax when supported */
@supports (animation-timeline: scroll()) {
.hero-bg {
animation: parallax linear both;
animation-timeline: scroll();
}
}
Performance and Accessibility
Parallax has two traps.
The first is performance. JavaScript scroll listeners that mutate styles every frame hog the main thread and cause jank. All three CSS techniques โ background-attachment, perspective, and scroll-driven animations โ run on the compositor layer, keeping the main thread free. If you absolutely must use JavaScript, limit work to elements inside the viewport using IntersectionObserver.
The second is accessibility. Parallax motion can trigger dizziness and nausea in users with vestibular disorders. Always respect prefers-reduced-motion.
@media (prefers-reduced-motion: reduce) {
.parallax-bg {
animation: none;
background-attachment: scroll;
transform: none;
}
}
Three lines. That's all it takes to make your parallax inclusive.
Takeaways
- Parallax creates a depth illusion by moving background and foreground layers at different scroll speeds
- background-attachment: fixed is the simplest approach but doesn't work on mobile browsers (iOS Safari, Android Chrome)
- perspective + translateZ creates true 3D parallax using CSS transforms, with scale() to compensate for apparent size reduction
- scroll-driven animations (animation-timeline: scroll()) are the modern, JS-free way to bind animations to scroll position
- Avoid JavaScript scroll listeners for parallax โ CSS-only techniques run on the compositor thread for better performance
- Always add a prefers-reduced-motion media query to disable parallax for users with vestibular sensitivities
- Use @supports to progressively enhance with scroll-driven animations while keeping a solid fallback for unsupported browsers