Smooth Parallax Scrolling
Create reasonably smooth parallax effects without JavaScript. Allegedly works in most browsers.
Performance note
This technique performs best with will-change: transform and modest layer counts. Your mileage may vary. Test on actual devices, not just your M3 MacBook.
Live Demo
Scroll this box. The background text moves slower than the foreground text.
Keep scrolling...
Still working.
No JavaScript involved.
The Problem
Most parallax implementations involve listening to scroll events in JavaScript, calculating positions, and updating styles on every frame. This works until you try it on a mid-range Android phone, at which point you discover what "janky" really means.
Turns out CSS has had the tools to do this natively since roughly 2015. We just keep forgetting.
The Solution
The trick involves perspective, transform: translateZ(), and scale(). Elements further back in 3D space scroll slower due to perspective distortion. We then scale them back up to compensate for the distance.
It's the kind of solution that makes you feel clever for about 30 seconds, then mildly concerned that it actually works.
<div class="parallax-container">
<div class="parallax-layer parallax-back">
<!-- Background content -->
</div>
<div class="parallax-layer parallax-base">
<!-- Foreground content -->
</div>
</div>Container Setup
First, set up a container with perspective. This defines the viewer's distance from the z=0 plane. Lower values create more dramatic effects. Higher values create subtler effects. 1px creates existential dread.
.parallax-container {
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
perspective: 1px;
perspective-origin: center center;
}Layer Configuration
Each layer gets a translateZ() value and a compensating scale(). The formula is: scale = 1 + (translateZ * -1) / perspective.
For translateZ(-1px) with perspective: 1px, that's scale(2). Math fans will enjoy this. Everyone else can copy-paste and move on with their lives.
.parallax-layer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.parallax-back {
transform: translateZ(-1px) scale(2);
z-index: 1;
}
.parallax-base {
transform: translateZ(0);
z-index: 2;
}Different Speeds
By adjusting the translateZ() value, you can control how much slower (or faster) each layer moves. Here's a demo with three different speeds:
Multiple Layers Demo
Three layers, three speeds. No JavaScript.
Notice the depth effect.
Each layer has its own speed.
translateZ(-2px) = slowest
translateZ(-0.5px) = medium
translateZ(0) = normal
Note: The FPS counter above uses JavaScript to measure performance, not to create the parallax effect. The irony is not lost on us.
Performance Optimizations
If you're feeling responsible (or your Lighthouse score is suffering), add these:
.parallax-layer {
will-change: transform;
backface-visibility: hidden;
transform-style: preserve-3d;
}
/* Disable on reduced-motion preference */
@media (prefers-reduced-motion: reduce) {
.parallax-container {
perspective: none;
}
.parallax-layer {
transform: none !important;
}
}Browser Support
Works in Chrome, Firefox, Safari, and Edge. IE11 support is left as an exercise for the historically minded. If you need to support IE11 in 2025, you have bigger problems than parallax scrolling.
When Not to Use This
- Complex interactions – If you need scroll-linked animations with precise timing, use JavaScript (or better yet, the Scroll-Driven Animations API if you're feeling modern).
- Accessibility concerns – Some users find parallax disorienting. Always respect
prefers-reduced-motion. - Low-end devices – Test thoroughly. What feels smooth on your development machine might struggle elsewhere.
Further Reading
This technique was popularized by Keith Clark's 2014 blog post, which remains one of the better explanations of CSS 3D transforms you'll find. Standing on the shoulders of giants, and all that.