10 CSS Parallax Effects 04 / 10

Multi-Scene Parallax Scrolling

Five full-bleed sections (Cosmos, Desert, Ocean, Forest, Finale) each with a slower-moving SVG background driving the parallax via a requestAnimationFrame-throttled scroll listener.

CSS + JS MIT licensed
Live Demo Open in tab

This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.

Open in playground

The code

<div class="plx-04" id="plx04">

  <nav class="plx-04__nav" aria-label="Scenes">
    <button class="plx-04__nav-dot is-active" aria-label="Scene 1" data-scene="0"></button>
    <button class="plx-04__nav-dot" aria-label="Scene 2" data-scene="1"></button>
    <button class="plx-04__nav-dot" aria-label="Scene 3" data-scene="2"></button>
    <button class="plx-04__nav-dot" aria-label="Scene 4" data-scene="3"></button>
    <button class="plx-04__nav-dot" aria-label="Scene 5" data-scene="4"></button>
  </nav>

  <!-- S1: Cosmos -->
  <div class="plx-04__scene plx-04__s1">
    <div class="plx-04__sticky">
      <div class="plx-04__bg" data-parallax="0.35"></div>
      <div class="plx-04__fg">
        <p class="plx-04__tag">CSS Parallax · Five Worlds</p>
        <h1 class="plx-04__h1">The <em>infinite</em><br>canvas awaits</h1>
        <p class="plx-04__p">Five worlds, each with its own sky. A full-page parallax journey — backgrounds drift at their own speed while you move through them.</p>
        <div class="plx-04__accent"></div>
        <div class="plx-04__scroll-hint">
          <span>Scroll</span>
          <div class="plx-04__arrow"></div>
        </div>
      </div>
    </div>
  </div>

  <!-- S2: Desert -->
  <div class="plx-04__scene plx-04__s2">
    <div class="plx-04__sticky">
      <div class="plx-04__bg" data-parallax="0.4"></div>
      <div class="plx-04__fg">
        <p class="plx-04__num">02 / Desert</p>
        <h2 class="plx-04__h">Heat &amp;<br><em>Silence.</em></h2>
        <p class="plx-04__p">The desert teaches patience. Vast emptiness between two points — just the relentless geometry of distance and will.</p>
      </div>
    </div>
  </div>

  <!-- S3: Ocean -->
  <div class="plx-04__scene plx-04__s3">
    <div class="plx-04__sticky">
      <div class="plx-04__bg" data-parallax="0.3"></div>
      <div class="plx-04__fg">
        <p class="plx-04__num">03 / Deep Ocean</p>
        <h2 class="plx-04__h">Pressure<br>builds <em>diamonds.</em></h2>
        <p class="plx-04__p">At depth, things become transparent. Clarity is not found in brightness — it's found in the dark, still water far below.</p>
      </div>
    </div>
  </div>

  <!-- S4: Forest -->
  <div class="plx-04__scene plx-04__s4">
    <div class="plx-04__sticky">
      <div class="plx-04__bg" data-parallax="0.45"></div>
      <div class="plx-04__fg">
        <p class="plx-04__num">04 / Ancient Forest</p>
        <h2 class="plx-04__h"><em>Roots</em><br>remember<br>everything.</h2>
        <p class="plx-04__p">The oldest trees have no memory of being seeds. They simply became what they were always going to be — slowly, in the green quiet.</p>
      </div>
    </div>
  </div>

  <!-- S5: Finale -->
  <div class="plx-04__scene plx-04__s5">
    <div class="plx-04__sticky">
      <div class="plx-04__bg" data-parallax="0.35"></div>
      <div class="plx-04__fg">
        <p class="plx-04__num">05 / Origin</p>
        <h2 class="plx-04__h">Every end<br>is an <em>origin.</em></h2>
        <p class="plx-04__p">The scroll ends. The journey doesn't. What you build with this moment is the only thing that matters now.</p>
      </div>
    </div>
  </div>

</div>
.plx-04, .plx-04 *, .plx-04 *::before, .plx-04 *::after {
  box-sizing: border-box; margin: 0; padding: 0;
}
.plx-04 {
  font-family: 'Outfit', sans-serif;
  background: #04020f;
  color: #f0ede8;
  /* overflow:clip not overflow:hidden — see Demo 09 + 10 fixes
     (commits 876bc78, this one). overflow:hidden creates a scroll
     context that breaks position:sticky on descendants. */
  overflow-x: clip;
}

/* Each section is a sticky-pinned scene: 200vh outer + 100vh
   sticky inner = 100vh of pin runway during which the bg parallax
   animation plays. Sticky pinning works correctly because the
   root .plx-04 uses overflow-x:clip (not :hidden), which would
   have broken the sticky containment chain. */
.plx-04__scene {
  position: relative;
  height: 200vh;
}

/* Each scene's outer container has a fallback bg color matching
   its theme. Without these, the 100vh of "post-sticky-release"
   scroll runway at the bottom of each scene's 200vh container
   would show the body bg (#04020f), creating a dark band between
   scenes as the visitor scrolls between them. The per-scene
   theme colors below match the dominant midtone of each scene's
   gradient bg, so the transition reads as one continuous scene
   color instead of a dark stripe gap. */
.plx-04__s1 { background: #07041a; }
.plx-04__s2 { background: #3d1800; }
.plx-04__s3 { background: #002040; }
.plx-04__s4 { background: #071407; }
.plx-04__s5 { background: #0e0028; }

.plx-04__sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
}

/* Background layer — moves slower than scroll (parallax effect via JS) */
.plx-04__bg {
  position: absolute;
  inset: -25%;
  will-change: transform;
}

/* Content — centered, normal scroll speed */
.plx-04__fg {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 40px;
  z-index: 5;
}

/* ── Scene 1: Cosmos ── */
.plx-04__s1 .plx-04__bg {
  background:
    radial-gradient(ellipse at 20% 20%, rgba(120,60,200,0.5) 0%, transparent 45%),
    radial-gradient(ellipse at 80% 75%, rgba(30,80,200,0.4) 0%, transparent 45%),
    linear-gradient(135deg, #04020f 0%, #0a0520 100%);
}
.plx-04__s1 .plx-04__bg::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(1.5px 1.5px at 8% 12%, #fff 0%, transparent 100%),
    radial-gradient(1px 1px at 25% 8%, rgba(255,255,255,0.8) 0%, transparent 100%),
    radial-gradient(2px 2px at 45% 20%, #fff 0%, transparent 100%),
    radial-gradient(1px 1px at 62% 6%, rgba(255,255,255,0.7) 0%, transparent 100%),
    radial-gradient(1.5px 1.5px at 78% 18%, #fff 0%, transparent 100%),
    radial-gradient(1px 1px at 12% 45%, rgba(255,255,255,0.6) 0%, transparent 100%),
    radial-gradient(2px 2px at 55% 55%, rgba(255,255,255,0.9) 0%, transparent 100%),
    radial-gradient(1px 1px at 35% 68%, rgba(255,255,255,0.7) 0%, transparent 100%),
    radial-gradient(1.5px 1.5px at 88% 40%, #fff 0%, transparent 100%),
    radial-gradient(1px 1px at 70% 72%, rgba(255,255,255,0.8) 0%, transparent 100%),
    radial-gradient(1px 1px at 18% 82%, rgba(255,255,255,0.6) 0%, transparent 100%),
    radial-gradient(1.5px 1.5px at 92% 85%, rgba(255,240,200,0.9) 0%, transparent 100%);
}
.plx-04__s1 .plx-04__bg::after {
  content: '';
  position: absolute;
  width: 260px; height: 260px;
  top: 18%; right: 20%;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, #fff8e1 0%, #ffe082 60%);
  box-shadow: 0 0 60px rgba(255,220,130,0.6), 0 0 140px rgba(255,180,80,0.25);
}

/* ── Scene 2: Desert ── */
.plx-04__s2 .plx-04__bg {
  background:
    radial-gradient(ellipse at 55% 30%, rgba(200,80,20,0.5) 0%, transparent 55%),
    radial-gradient(ellipse at 20% 80%, rgba(140,60,0,0.3) 0%, transparent 45%),
    linear-gradient(160deg, #1a0800 0%, #3d1800 40%, #6b2d00 100%);
}
.plx-04__s2 .plx-04__bg::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(45deg, transparent, transparent 40px, rgba(255,150,50,0.04) 40px, rgba(255,150,50,0.04) 41px),
    repeating-linear-gradient(-45deg, transparent, transparent 80px, rgba(200,100,20,0.03) 80px, rgba(200,100,20,0.03) 81px);
}
.plx-04__s2 .plx-04__bg::after {
  content: '';
  position: absolute;
  bottom: 15%;
  left: -5%;
  right: -5%;
  height: 35%;
  background: repeating-linear-gradient(
    0deg,
    rgba(180,80,20,0.08) 0px,
    rgba(180,80,20,0.12) 4px,
    transparent 4px,
    transparent 18px
  );
  transform: skewY(-3deg);
}

/* ── Scene 3: Ocean ── */
.plx-04__s3 .plx-04__bg {
  background:
    radial-gradient(ellipse at 25% 55%, rgba(0,120,200,0.5) 0%, transparent 50%),
    radial-gradient(ellipse at 75% 25%, rgba(0,200,160,0.25) 0%, transparent 45%),
    linear-gradient(180deg, #001020 0%, #002040 55%, #003060 100%);
}
.plx-04__s3 .plx-04__bg::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(-30deg, transparent, transparent 50px, rgba(0,150,255,0.05) 50px, rgba(0,150,255,0.05) 52px);
}
.plx-04__s3 .plx-04__bg::after {
  content: '';
  position: absolute;
  top: 40%;
  left: -5%;
  right: -5%;
  height: 3px;
  background: linear-gradient(90deg, transparent, rgba(0,200,180,0.4), transparent);
  box-shadow: 0 20px 0 rgba(0,150,200,0.1), 0 -20px 0 rgba(0,150,200,0.1);
}

/* ── Scene 4: Forest ── */
.plx-04__s4 .plx-04__bg {
  background:
    radial-gradient(ellipse at 50% 0%, rgba(80,200,70,0.2) 0%, transparent 55%),
    radial-gradient(ellipse at 20% 60%, rgba(40,120,40,0.2) 0%, transparent 45%),
    linear-gradient(180deg, #020b02 0%, #071407 55%, #0a1e0a 100%);
}
.plx-04__s4 .plx-04__bg::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(90deg, transparent, transparent 80px, rgba(50,150,50,0.04) 80px, rgba(50,150,50,0.04) 82px),
    repeating-linear-gradient(0deg, transparent, transparent 60px, rgba(30,100,30,0.04) 60px, rgba(30,100,30,0.04) 62px);
}

/* ── Scene 5: Finale ── */
.plx-04__s5 .plx-04__bg {
  background:
    radial-gradient(ellipse at 50% 45%, rgba(200,80,255,0.35) 0%, transparent 55%),
    radial-gradient(ellipse at 20% 20%, rgba(80,120,255,0.25) 0%, transparent 45%),
    linear-gradient(135deg, #08001a 0%, #180030 100%);
}

/* ── Typography ── */
.plx-04__tag {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.4em;
  text-transform: uppercase;
  color: rgba(255,255,255,0.35);
  margin-bottom: 20px;
}

.plx-04__num {
  font-family: 'Outfit', sans-serif;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.4em;
  text-transform: uppercase;
  color: rgba(255,255,255,0.3);
  margin-bottom: 16px;
}

.plx-04__h1 {
  font-family: 'Playfair Display', serif;
  font-size: clamp(52px, 11vw, 140px);
  font-weight: 700;
  line-height: 0.9;
  letter-spacing: -0.02em;
}
.plx-04__h1 em {
  font-style: italic;
  font-weight: 400;
  background: linear-gradient(135deg, #fbbf24, #f59e0b);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  background-clip: text;
}

.plx-04__h {
  font-family: 'Playfair Display', serif;
  font-size: clamp(42px, 8vw, 100px);
  font-weight: 700;
  line-height: 0.95;
}
.plx-04__s2 .plx-04__h em { color: #fb923c; font-style: italic; }
.plx-04__s3 .plx-04__h em { color: #38bdf8; font-style: italic; }
.plx-04__s4 .plx-04__h em { color: #86efac; font-style: italic; }
.plx-04__s5 .plx-04__h em { color: #e879f9; font-style: italic; }

.plx-04__p {
  margin-top: 24px;
  font-size: 16px;
  font-weight: 300;
  line-height: 1.75;
  opacity: 0.5;
  max-width: 480px;
}

.plx-04__scroll-hint {
  position: absolute;
  bottom: 36px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 10px;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  opacity: 0.35;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.plx-04__arrow {
  width: 18px; height: 18px;
  border-right: 1px solid rgba(255,255,255,0.4);
  border-bottom: 1px solid rgba(255,255,255,0.4);
  transform: rotate(45deg);
  animation: plx-04-bounce 1.6s ease-in-out infinite;
}
@keyframes plx-04-bounce {
  0%, 100% { transform: rotate(45deg) translateY(0); }
  50% { transform: rotate(45deg) translateY(6px); }
}

/* Nav dots */
.plx-04__nav {
  position: fixed;
  right: 20px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.plx-04__nav-dot {
  display: block;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: rgba(255,255,255,0.2);
  border: none;
  cursor: pointer;
  transition: all 0.3s;
}
.plx-04__nav-dot.is-active {
  background: rgba(255,255,255,0.85);
  transform: scale(1.7);
}

/* Accent lines that appear at each scene */
.plx-04__accent {
  width: 1px;
  height: 60px;
  background: linear-gradient(to bottom, rgba(255,255,255,0.5), transparent);
  margin: 28px auto 0;
}

@media (max-width: 600px) {
  .plx-04__nav { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .plx-04__bg { transform: none !important; }
  .plx-04__arrow { animation: none; }
}
(() => {
  const root = document.getElementById('plx04');
  if (!root) return;

  const bgs = Array.from(root.querySelectorAll('[data-parallax]')).map(el => ({
    el,
    speed: parseFloat(el.dataset.parallax),
    sticky: el.closest('.plx-04__sticky'),
    scene: el.closest('.plx-04__scene'),
  }));

  const dots = Array.from(root.querySelectorAll('.plx-04__nav-dot'));
  const scenes = Array.from(root.querySelectorAll('.plx-04__scene'));

  // Nav dot click — scroll to scene start
  dots.forEach((dot, i) => {
    dot.addEventListener('click', () => {
      scenes[i]?.scrollIntoView({ behavior: 'smooth' });
    });
  });

  let ticking = false;
  function onScroll() {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => {
      const sy = window.scrollY;
      const vh = window.innerHeight;

      // Parallax: bg moves slower than scroll
      bgs.forEach(({ el, speed, scene }) => {
        const rect = scene.getBoundingClientRect();
        if (rect.bottom < -vh || rect.top > vh * 2) { ticking = false; return; }
        // Progress within the scene's sticky section
        const sceneTop = scene.offsetTop;
        const progress = sy - sceneTop;
        el.style.transform = `translateY(${progress * speed * -1}px)`;
      });

      // Active dot
      let activeI = 0;
      scenes.forEach((s, i) => {
        const r = s.getBoundingClientRect();
        if (r.top <= vh * 0.5) activeI = i;
      });
      dots.forEach((d, i) => d.classList.toggle('is-active', i === activeI));

      ticking = false;
    });
  }

  window.addEventListener('scroll', onScroll, { passive: true });
  onScroll();
})();

Search CodeFronts

Loading…