What Is CSS Parallax and Which Methods Create It?
Depth comes from the speed gap. A background layer travels slower than the text in front of it, and the eye reads that gap as distance, the same illusion designers know as parallax scrolling.
The older route ran on scripts. A JavaScript scroll listener read the position and rewrote transform values on every layer. CSS methods hand that work to the browser’s own scrolling and compositing.
| Method | Mechanism | Main failure mode |
|---|
| background-attachment: fixed | Background pinned to the viewport | Partial support on iOS Safari |
| Perspective with translateZ | 3D depth inside a scroll container | overflow: hidden on a group flattens the 3D scene |
| Scroll-driven animations | Keyframes tied to scroll position | Missing in Firefox before version 159 |
| Sticky with transform | Layer pinned inside a tall wrapper | Ancestor overflow stops the sticking |
None of the four covers every case. Fixed backgrounds take the least code. Perspective is the one with real layered depth. Scroll-driven animations have the cleanest markup, and sticky is handy when the effect has to stay inside its own section.
How to Build Parallax with background-attachment: fixed
Add background-attachment: fixed to an element with a background image, and the image stays pinned to the viewport while content scrolls over it. One declaration produces the effect.
The usual setup is a hero image behind a headline, with normal content scrolling past it.
.parallax {
background-image: url("hero.jpg");
background-attachment: fixed;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
min-height: 500px;
}
Alternating parallax bands with plain “buffer” sections is a design choice, not a requirement (LogRocket describes this banded layout). min-height sets how much of the picture is visible per band. And because cover sizes the image against the viewport when the attachment is fixed, the source file needs to be at least screen-sized.
Can I use lists iOS Safari as partial support in every version from 5.0 through 27.2, with a note that fixed is recognized but has no effect.
WebKit’s bug tracker (bug 234779) spells out what partial means. The background scrolls with the page but is still cropped as if it were fixed. The same report notes that @supports claims the property is supported, so a feature query cannot be relied on to gate it.
Turn the effect off with media queries and fall back to background-attachment: scroll. A width breakpoint misses large tablets, so (pointer: coarse) targets touch screens more directly.
The upside is how little it takes. One declaration, no transforms, and it works on any element that has a background, with no scroll container to restructure.
The downsides are real, though. It’s broken on iOS Safari. You also get one layer only, always at zero speed relative to the viewport, and background images carry no alt text.
How Does the Perspective and translateZ Method Work?
Keith Clark described this technique in his August 2014 article on pure CSS parallax websites. Layers render at different depths inside one 3D scene, so scrolling moves distant layers slower than near ones. It needs a scroll container with a perspective value and children offset on the Z axis, and the pushed-back layers get a scale() correction so they keep their original size.
The W3C CSS Transforms Module Level 2 defines the properties involved, which are perspective, perspective-origin, transform-style and translateZ.
Setting Up the Scroll Container
The scroll container, not the page, supplies the 3D viewport. Per Clark, giving an element a height plus the CSS perspective property creates a fixed 3D viewport centered on it.
- Create the container with height: 100vh, overflow-x: hidden, overflow-y: auto and perspective: 1px.
- Add a group wrapper per section with position: relative and transform-style: preserve-3d.
- Place each layer inside its group with position: absolute and all four offsets at 0.
- Give the base layer translateZ(0) so it scrolls at normal speed.
- Give the background layer a negative translateZ, such as -1px, plus the matching scale value.
- Keep the text on the base layer.
Calculating the Scale Correction
Pushing a layer back shrinks it, so it needs scale() to render at its original size. Clark’s formula is scale = 1 + (translateZ × -1) / perspective.
| translateZ (perspective 300px) | Scale | Scroll speed vs page |
|---|
| 90px | 0.7 | 1.43x (faster) |
| 0 | 1 | 1x |
| -300px | 2 | 0.5x |
| -600px | 3 | 0.33x |
The translateZ values come from Clark’s demo. The speed column follows from the formula, since a layer’s scroll speed is the inverse of its scale.
perspective-origin moves the vanishing point. Clark’s article shows perspective-origin-x: 100% to shift it to the right edge.
Pros and Cons of the Perspective Method
You get real multi-layer depth with no scripts, and each layer’s speed comes down to one number.
The catch is that the page scrolls inside a div, so window scroll position stays at 0. overflow: hidden on a group breaks the effect, per Clark. Scaled layers also grow, so background images need oversized source files.
How to Create Parallax with Scroll-Driven Animations
Scroll-driven animations link a keyframe animation to scroll progress through animation-timeline, so the animation advances with the scrollbar instead of the clock. For parallax, that means a transform animation on a background layer.
@keyframes drift {
from { transform: translateY(-15%); }
to { transform: translateY(15%); }
}
.parallax-bg {
animation: drift linear both;
animation-timeline: view();
}
The animation is an ordinary set of CSS keyframes. Linear easing keeps the motion proportional to scroll distance.
scroll() gives you a scroll progress timeline, which tracks how far a scroll container has scrolled. MDN lists nearest, root and self as scroller values. view() is a view progress timeline that tracks an element’s passage through the scrollport, and it suits section-level parallax because each section animates on its own schedule.
animation-range limits where on the timeline the animation runs, using named ranges such as cover, contain, entry and exit. Declare animation-timeline and animation-range after the animation shorthand, because the shorthand resets the longhands it leaves out to their initial values (animation-timeline returns to auto, per MDN).
Named timelines come from scroll-timeline-name or view-timeline-name. By default, a named timeline can only be used by descendants of the element that declares it (Chrome for Developers), and timeline-scope extends that reach.
Wrap the rules in @supports (animation-timeline: scroll()). Browsers without support keep the static layer, which is a fine failure for decoration.
A polyfill exists, but it moves the work back to JavaScript and removes the main reason to choose this method.
Chrome 115 added the CSS properties and the ScrollTimeline and ViewTimeline JavaScript classes (Chrome for Developers guide), and Safari 26.0 adds scroll-driven animations (WebKit release notes). There are 2 timeline types, scroll progress and view progress. The guide names scroll-timeline-name, view-timeline-name, animation-timeline and animation-range as the properties involved. Firefox 159, scheduled for October 27, 2026, enables scroll-driven animations by default (MDN’s Firefox 159 release notes), which puts the feature in all three engines. MDN had not yet marked scroll() or animation-range as Baseline at the time of writing.
The markup stays clean, it runs on normal window scroll, and per-layer speed comes from the keyframes. What you give up is browser reach, so a static fallback is needed, and named timelines come with scope rules.
How Do Position Sticky and Transform Create Layered Scroll Effects?
A sticky layer inside a tall wrapper stays put while content scrolls over it, which is parallax at zero speed. Add a transform driven by a scroll-driven animation and the layer drifts at a fraction of page speed.
That dependency matters. Where scroll-driven animations are unsupported, the layer stays pinned and never drifts.
The gain over a fixed background is containment. A sticky layer stops at the bottom edge of its containing block, so the effect ends with its wrapper.
The usual way this breaks is ancestor overflow. MDN positions a sticky element against its nearest scrolling ancestor, so overflow: hidden, auto or scroll on any ancestor makes it the reference, and the layer stops sticking. overflow: clip fixes that, because it clips content without creating a scroll container, as Ben Frain’s write-up of MDN’s definition points out.
Size is the other snag. A sticky element larger than its scroll container cannot hold its position, and once you scroll far enough, the top portion that had stuck scrolls away (Frontend Masters).
Pick sticky when the layer should be a real element, such as an SVG or an img with alt text, rather than a CSS background.
Which CSS Parallax Method Should You Choose?
Perspective suits layered depth, and scroll-driven animations suit a single drifting layer on current browsers. A JavaScript library only makes sense when CSS can’t express the motion, and most pages never reach that stage.
For one background image and a desktop audience, background-attachment: fixed is enough. Three or more layers with real depth point to perspective with translateZ. One or two drifting layers on current browsers fit scroll-driven animations behind @supports, and sticky positioning is the pick for a layer that must end with its section.
Skip the fixed background whenever phone traffic matters. iOS Safari gives it no effect in any version, even though it is the shortest of the four to write.
A library earns its weight when the effect needs scrubbed timelines, pinned sections, or sequencing across many elements. The other trigger is browser reach. Scroll-driven animations still carry a cross-browser compatibility gap, and a script closes it at the cost of weight.
| Library | What it does | Note |
|---|
| GSAP ScrollTrigger | Scrubbed, pinned, sequenced scroll animation | Free, commercial use included (current README) |
| Rellax.js | Per-element parallax speed through a data attribute | Roughly 2.5 KB gzipped, v1.12.1 (Bundlephobia) |
| Jarallax | Parallax for background images and video (YouTube, Vimeo, self-hosted) | disableParallax option for mobile |
| Locomotive Scroll | Smooth scrolling, in-view detection, data-scroll-speed parallax | Version 5 is built on Lenis |
| Lenis | Smooth scrolling layer | Often paired with ScrollTrigger |
Licensing advice conflicts between sources. Older GreenSock forum answers say sites that charge multiple customers need a Business Green membership, while the current GSAP README says the whole toolset, plugins included, is free for commercial use since Webflow’s acquisition.
Read the license text for the exact version before shipping a paid product.
CSS alone is enough when the effect is decoration, not choreography, and a static fallback is acceptable on older browsers. It also wins when script weight counts against a performance budget.
A library makes sense when scroll position must drive timelines, pins or sequences, or when browsers below the current generation are a hard requirement. Tuning speed per element at runtime is another reason.
The price of a library is script weight plus main-thread work on every scroll frame. Build the static mobile layout first, as in mobile-first design, and add either approach on top.
Does CSS Parallax Hurt Page Performance?
Not when the motion stays on transform. Transform-based CSS parallax stays cheap, and the cost rises with layer count and with any property that forces paint or layout.
Compositor-Friendly Properties
web.dev’s rendering guidance names 2 properties that the compositor can animate without layout or paint, transform and opacity.
translateY, translateZ and scale all count as transform, so the perspective and scroll-driven methods stay inside that budget. Animating background-position, top or margin does not.
Can I use adds a note on the fixed-background method. Most mobile devices show a delay in updating the background position after scrolling.
Layer and Memory Cost
MDN warns that excessive will-change use causes excessive memory use, and advises against adding it to pages that already perform well.
Each compositor layer takes memory too, and on low-memory devices the cost can outweigh the gain (web.dev). For compositing time during scrolling, web.dev suggests aiming for roughly 4-5ms.
Each translateZ layer in the perspective method is typically promoted to its own compositor layer. Count layers, not sections.
Layout Shift and Core Web Vitals
When a parallax section has no reserved height, the image loads, the section grows, and everything below it jumps. That’s CLS, and web.dev sets the good threshold at 0.1.
Set min-height or aspect-ratio on every parallax container and declare width and height on the layer images. The hero image should load eagerly, since it is usually the LCP element.
Measuring the Cost
Open the Layers view in Chrome DevTools to count layers and see why each one exists. Then record a scroll in the Performance panel to catch layout shifts, and run Lighthouse for a lab CLS reading.
How to Make CSS Parallax Accessible
Run the motion only under prefers-reduced-motion: no-preference, serve a static layout to everyone else, and treat WCAG SC 2.3.3 as the standard to meet. The fallback is part of the build, not a patch.
Motion triggers real symptoms. Vestibular reactions can include dizziness and nausea, and web.dev also lists migraine headaches, some serious enough to need bed rest.
Motion is one barrier among many in web accessibility work, but it is the one parallax creates by design.
The W3C’s Understanding document for SC 2.3.3 names parallax scrolling as often non-essential animation. The criterion sits at Level AAA, arrived with WCAG 2.1, and carries into WCAG 2.2.
SC 2.2.2 (Pause, Stop, Hide) is the other motion criterion. It covers animation the page starts on its own that runs longer than five seconds, so scroll-driven parallax falls under 2.3.3 instead.
- Write the base CSS with no motion, so the static layout is the default.
- Put the parallax rules inside @media (prefers-reduced-motion: no-preference), so motion is opt-in.
- For scroll-driven animations, nest the @supports test inside that same media query.
- For the perspective method, set perspective: none on the container and transform: none on the layers under (prefers-reduced-motion: reduce).
- For libraries, check window.matchMedia(“(prefers-reduced-motion: reduce)”) and skip initialization, or call destroy() where one exists (Rellax resets elements to their original positions).
- Test the reduced setting in DevTools and on a real device.
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: scroll()) {
.parallax-bg {
animation: drift linear both;
animation-timeline: view();
}
}
}
Visitors turn the preference on in their OS settings, and the path differs by system.
- macOS: System Settings, Accessibility, Display, Reduce motion
- iOS and iPadOS: Settings, Accessibility, Motion, Reduce Motion
- Windows 11: Settings, Accessibility, Visual effects, Animation effects
- Windows 10: Settings, Ease of Access, Display, Show animations in Windows
- Android: Accessibility, Remove animations
In Chrome DevTools, the command menu runs Emulate CSS prefers-reduced-motion: reduce, which tests the CSS layer without touching OS settings.
A static layout is the safest fallback. Craft CMS’s design guidance offers a fade-in as a polished alternative.
When Does CSS Parallax Break or Not Apply?
Drop it on pages built for reading or form-filling, and anywhere layout height is unstable. In those cases the effect adds cost and takes something away.
Long articles, documentation and legal text gain nothing from drifting backgrounds, so use a static hero there. On forms and checkout, motion behind inputs competes with the task. A flat background works better.
Text over a moving image is a problem because contrast changes frame by frame, and a scrim behind the text helps. Full-height sections on phones overshoot the visible screen with 100vh, so use svh. Every layer costs memory on low-memory devices, which means cutting the layer count. Reduced-motion users get the static layout, no exceptions.
The mobile viewport changes as toolbars collapse, which is why web.dev calls 100vh too tall on load.
svh sizes to the small viewport (toolbars shown), lvh to the large one, and dvh follows the toolbar as it moves. dvh does not update at 60fps (web.dev), and a dvh-sized element reflows during scroll, so svh suits always-on hero sections.
A caption that clears color contrast over the sky in a photo can fail over the dark ground a few hundred pixels later. Test the worst frame, not the first one.
A scrim layer with fixed opacity behind the text removes the variable.
CSS Parallax FAQ
Can CSS parallax run horizontally?
Yes. Scroll-driven animations accept an inline axis through scroll(inline) and view(inline), so a horizontal scroller can drive translateX the way vertical scrolling drives translateY.
Rellax supports horizontal parallax too, but its README keeps it disabled by default.
How many layers are too many?
No source sets a fixed limit. Each compositor layer takes memory (web.dev), and the cost shows first on low-memory phones.
Start with 2 or 3 layers per section, then check the Layers view in Chrome DevTools and scroll on a mid-range phone.
Does parallax affect SEO?
Parallax is a presentation effect, not a ranking signal. Its side effects matter more, since layout shift and heavy hero images feed Core Web Vitals.
Text that exists only inside a background image cannot be read as text, so keep headlines in HTML.
What image size and format suit parallax layers?
Export each layer at the width it renders, not the width of its box. A layer scaled by 2 renders at twice its box, so a small source turns soft.
Compress the file as WebP or AVIF, and load the hero layer eagerly.
Does parallax work in WordPress page builders like Elementor?
Yes, with limits. Builders that rely on background-attachment: fixed inherit the iOS limitation, and Elementor’s help center says so.
Elementor recommends its Motion Effects instead, or setting the attachment back to Default on mobile.
What to Test Before Shipping CSS Parallax
Start with the reduced-motion fallback, then check a real phone. After that, look at the layer and layout-shift profile, and finish with text contrast at the worst frame.
The order follows who gets excluded. Without a fallback, visitors with vestibular disorders get hurt. A broken phone layout affects every mobile visitor, and too many layers mostly punish low-memory devices.
Contrast comes last because it is the only check that a scrim layer fixes in minutes.
Visitors on reduced-motion settings and on browsers without scroll-driven animations see a flat page, and that’s by design. The layout has to hold up without the motion.
As of October 2026, MDN does not yet mark scroll-driven animations as Baseline, although Firefox 159 enables them by default from October 27, 2026. Keep the static fallback in the build until older Firefox versions drop out of your analytics.
The same reduced-motion gating applies to every scroll-linked effect, including those in the guide to CSS animations on scroll.