How to Create a Parallax Scrolling Effect With CSS Only (No JavaScript)

A CSS parallax effect happens when the background of a page moves slower than the content in front of it while you scroll, which tricks the eye into seeing depth on a flat screen. Most tutorials solve this with a JavaScript library that listens to every scroll event, recalculates positions and quietly destroys your Interaction to Next Paint score. See geeksforgeeks.org for their take.

You do not need any of that. In 2026, the browser can handle parallax natively with three techniques: background-attachment, 3D transforms with perspective and transform-style, and scroll-driven animations. This guide gives you working code for each one, plus the mobile fallbacks and accessibility rules that almost every other tutorial skips.

The three CSS-only parallax methods compared

Method Best for Difficulty Support Performance
background-attachment: fixed Hero banners, section dividers Beginner Universal on desktop, broken on iOS Can trigger repaints on large images
3D transforms (perspective + translateZ) Multi-layer scenes, different speeds per layer Intermediate Every modern browser Excellent, GPU composited
Scroll-driven animations (animation-timeline) Precise control, any property, modern sites Intermediate Chromium, Safari 26+, Firefox (recent versions) Excellent, runs off the main thread
web design

Method 1: the two-line parallax hero with background-attachment

This is the fastest way to get a parallax hero online. The background image stays locked to the viewport while the section scrolls over it, so the image appears to move slower than the page.

HTML

<section class="parallax-hero">
  <div class="parallax-hero__content">
    <h1>Build faster interfaces</h1>
    <p>No JavaScript, no scroll listeners, no layout thrashing.</p>
  </div>
</section>

<section class="content">
  <p>Your regular page content goes here.</p>
</section>

CSS

.parallax-hero {
  min-height: 100svh;
  display: grid;
  place-items: center;
  background-image: linear-gradient(rgba(0,0,0,.45), rgba(0,0,0,.45)),
                    url("/img/hero-1920.webp");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  background-attachment: fixed;
}

.parallax-hero__content {
  color: #fff;
  text-align: center;
  max-width: 40rem;
  padding: 2rem;
}

/* Fallback: iOS Safari and touch devices */
@media (max-width: 768px), (hover: none) and (pointer: coarse) {
  .parallax-hero { background-attachment: scroll; }
}

/* Accessibility: respect motion preferences */
@media (prefers-reduced-motion: reduce) {
  .parallax-hero { background-attachment: scroll; }
}

Why the fallback matters: iOS Safari has never implemented background-attachment: fixed the way desktop browsers do. Without the media query above, the image is scaled to the full document height and looks zoomed in or jumps as the address bar collapses. Switching to scroll on touch devices gives you a clean static background instead of a broken one.

Bonus: a parallax content divider

The same idea works beautifully as a narrow band between two text sections. Keep it short so the page keeps its rhythm.

.parallax-divider {
  min-height: clamp(220px, 45vh, 420px);
  background-image: url("/img/divider-1600.webp");
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
  display: grid;
  place-items: center;
}

.parallax-divider blockquote {
  color: #fff;
  font-size: clamp(1.25rem, 3vw, 2rem);
  text-shadow: 0 2px 12px rgba(0,0,0,.6);
  max-width: 45ch;
  text-align: center;
}

@media (max-width: 768px), (prefers-reduced-motion: reduce) {
  .parallax-divider { background-attachment: scroll; }
}

Method 2: true multi-layer parallax with perspective and transform-style

If you want several layers moving at different speeds (a sky, mountains, trees, foreground text), background-attachment is not enough. The classic pure CSS solution uses a 3D scene: you push layers away from the camera on the Z axis, and because they are further away, they appear to move slower when the scroller moves.

The math you need

When you push an element back with translateZ, it also shrinks. You compensate with scale():

scale = 1 + (translateZ * -1) / perspective
  • perspective: 10px and translateZ(-10px) gives scale(2) and a layer moving at roughly half speed
  • perspective: 10px and translateZ(-20px) gives scale(3) and a layer moving at roughly one third speed
  • translateZ(0) means normal speed, which is your foreground

HTML

<div class="parallax">
  <section class="parallax__group">
    <div class="parallax__layer parallax__layer--back">
      <img src="/img/mountains.webp" alt="" width="1920" height="1080">
    </div>
    <div class="parallax__layer parallax__layer--base">
      <h1>Depth without a single line of JS</h1>
    </div>
  </section>

  <section class="parallax__group parallax__group--plain">
    <p>Normal scrolling content.</p>
  </section>
</div>

CSS

.parallax {
  height: 100svh;
  overflow-x: hidden;
  overflow-y: auto;
  perspective: 10px;
  perspective-origin: 0 0;
  /* keeps native scroll snapping and momentum intact */
  scroll-behavior: smooth;
}

.parallax__group {
  position: relative;
  height: 100svh;
  transform-style: preserve-3d;
}

.parallax__layer {
  position: absolute;
  inset: 0;
  transform-origin: 0 0;
}

.parallax__layer--back {
  transform: translateZ(-10px) scale(2);
  z-index: 1;
}

.parallax__layer--base {
  transform: translateZ(0);
  z-index: 2;
  display: grid;
  place-items: center;
  color: #fff;
}

.parallax__layer img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (prefers-reduced-motion: reduce) {
  .parallax { perspective: none; }
  .parallax__layer--back { transform: none; }
}

Things to watch with the 3D method

  1. The scroll container is the element with perspective, not the body. Anchor links, sticky headers and position: fixed elements behave differently inside it, so test your navigation.
  2. Stacking order is controlled by z-index inside each group. A layer pushed back on Z will still paint above another layer if its z-index is higher.
  3. Do not nest transforms on parents. Any ancestor with its own transform, filter or will-change creates a new containing block and flattens your 3D scene.
web design

Method 3: scroll-driven animations, the modern way

Scroll-driven animations are the cleanest option available today. You write a normal @keyframes rule and then tell the browser to drive it with scroll progress instead of time, using animation-timeline. The animation runs on the compositor, so it does not block the main thread the way a JavaScript scroll handler does.

Parallax hero with view()

@keyframes parallax-shift {
  from { transform: translate3d(0, -12%, 0); }
  to   { transform: translate3d(0, 12%, 0); }
}

.hero {
  position: relative;
  min-height: 100svh;
  overflow: hidden;
  isolation: isolate;
}

.hero__bg {
  position: absolute;
  inset: -15% 0;
  z-index: -1;
  background: url("/img/hero-1920.webp") center / cover no-repeat;
}

@supports (animation-timeline: view()) {
  .hero__bg {
    animation: parallax-shift linear both;
    animation-timeline: view();
    animation-range: cover 0% cover 100%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hero__bg { animation: none; }
}

Whole-page background driven by scroll()

Use scroll() when you want an element to react to the progress of the nearest scroller rather than its own visibility.

@keyframes drift {
  to { background-position: center 70%; }
}

.page-bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  background: url("/img/texture.webp") center 30% / cover no-repeat;
  animation: drift linear both;
  animation-timeline: scroll(root block);
}

Wrapping the rules in @supports (animation-timeline: view()) is the safest pattern: browsers that do not understand the syntax simply render a static background, which is a perfectly acceptable experience.

Performance and Core Web Vitals

Parallax gets a bad reputation because of JavaScript implementations that run heavy calculations on every scroll frame. A CSS-only approach removes that risk, but the images can still hurt you. Here is the checklist we apply on production sites:

  • Serve modern formats. WebP or AVIF, and keep hero images under roughly 200 KB. A hero background is usually the Largest Contentful Paint element.
  • Preload the hero image. <link rel="preload" as="image" href="/img/hero-1920.webp" fetchpriority="high"> shortens LCP noticeably when the image comes from CSS.
  • Reserve space. Give parallax sections an explicit min-height so nothing shifts when the image loads. That protects your Cumulative Layout Shift score.
  • Animate only transform and opacity. Animating top, margin or background-position on large surfaces forces layout and paint on every frame.
  • Use will-change sparingly. One or two layers at most. Every promoted layer consumes GPU memory, and on low-end Android phones that backfires.
  • Prefer 100svh over 100vh so mobile browsers do not jump when the address bar hides.
  • Add content-visibility: auto to parallax sections far below the fold to skip their rendering work until needed.
  • Limit yourself to two or three parallax zones per page. More than that feels gimmicky and multiplies the paint cost.
web design

Mobile fallbacks that actually work

Touch devices are where parallax breaks most often. Scroll events on mobile are asynchronous, the viewport height changes as toolbars collapse, and iOS still refuses to composite fixed backgrounds the way desktop does. Three rules keep things clean:

  1. Detect capability, not width. @media (hover: none) and (pointer: coarse) is a better proxy for touch devices than a pixel breakpoint, though combining both is fine.
  2. Degrade to a static background. A sharp, well-cropped static image beats a stuttering parallax every time.
  3. Keep the text readable. A gradient overlay on top of the image guarantees contrast whichever fallback is active.
@media (hover: none) and (pointer: coarse) {
  .parallax { perspective: none; overflow: visible; height: auto; }
  .parallax__group { height: auto; transform-style: flat; }
  .parallax__layer { position: static; transform: none; }
  .parallax-hero, .parallax-divider { background-attachment: scroll; }
}

Accessibility rules you should not skip

  • Always honour prefers-reduced-motion: reduce. Parallax is a common trigger for vestibular discomfort, dizziness and nausea.
  • Keep movement subtle. Shifts of 10 to 20 percent feel premium, shifts of 50 percent feel like a carnival ride.
  • Never attach meaning to a moving layer. Decorative images get alt="", important content stays in normal document flow.
  • Check that keyboard focus and anchor links still scroll correctly, especially with the 3D container method.
web design

Common mistakes and quick fixes

Symptom Likely cause Fix
Background looks zoomed on iPhone iOS ignores fixed attachment Switch to background-attachment: scroll on coarse pointers
Layers look flat, no depth Missing transform-style: preserve-3d or a transform on an ancestor Add preserve-3d, remove transforms from parents
Horizontal scrollbar appears Scaled back layer overflows overflow-x: hidden on the scroll container
Scroll feels janky Oversized image or too many promoted layers Compress the image, drop extra will-change declarations
Animation does nothing Browser without scroll-driven animation support Wrap the rules in @supports and provide a static state

Which method should you pick?

  • Marketing landing page, deadline tomorrow: Method 1. Two properties, works everywhere, one media query for mobile.
  • Illustrated storytelling page with several depth layers: Method 2. The 3D scene gives you real control over speed per layer.
  • New build, modern audience, progressive enhancement mindset: Method 3. Cleanest syntax, best performance profile, and it degrades gracefully by design.

You can also combine them: a scroll-driven hero at the top, a simple fixed-background divider further down. The important part is that none of it requires a scroll listener, a resize observer or a 30 KB library.

FAQ

How do you create a parallax effect?

Make the background move slower than the foreground. In CSS you can do that in three ways: set background-attachment: fixed on a section, push layers back with translateZ inside a container that has perspective and transform-style: preserve-3d, or drive a transform keyframe with animation-timeline: view(). All three work without JavaScript.

Can you give an example of a parallax effect?

A hero banner with a mountain photo behind a headline. As you scroll, the headline moves up at normal speed while the mountains drift up at half speed, so the mountains feel further away. The same pattern is used for quote dividers between article sections and for illustrated product pages with sky, hills and foreground layers. There’s a good explainer over at tympanus.net.

What are the disadvantages of parallax?

The main ones are motion sickness for sensitive users, extra image weight that can slow down Largest Contentful Paint, broken behaviour on iOS with fixed backgrounds, and a risk of pushing important content below the fold. Overused parallax also makes pages feel slow to navigate. Keep effects subtle, limit them to two or three per page and always support prefers-reduced-motion.

Does a CSS parallax effect hurt SEO?

Not by itself. The HTML stays crawlable because nothing is injected by scripts. What can hurt SEO is the image payload and layout instability, so compress your backgrounds, preload the hero and reserve height with min-height.

Is background-attachment: fixed bad for performance?

It can be on very large images, because the browser may repaint the background region while scrolling. On a single hero or a short divider with a compressed WebP file it is fine. For multiple full-screen sections, prefer the 3D transform method or scroll-driven animations, which are composited on the GPU. w3schools.com walks through the specifics.

Do scroll-driven animations work in every browser?

Chromium browsers and Safari 26 support animation-timeline, and Firefox has been shipping support in recent releases. Always guard the rules with @supports (animation-timeline: view()) so older browsers fall back to a static background instead of a broken layout.

How much movement is the right amount?

Aim for a speed difference of roughly 20 to 50 percent between background and foreground, or a translate range of 10 to 20 percent of the element height. Anything more tends to feel unstable and increases the chance of motion discomfort.

Recent Posts

No Posts Found!

Categories

Tags

    Subscribe

    You have been successfully Subscribed! Ops! Something went wrong, please try again.

    About Us

    Express Jam Studio was founded in 2004 by John Smith. John had previously worked for a courier company, but he saw an opportunity to start his own business in the web design and development industry.

    Contact Info

    Copyright © 2022 Express Jam Studio. All Rights Reserved.