10 CSS Parallax Effects 06 / 10

CSS Horizontal Parallax Scroll

Vertical scroll drives a five-panel horizontal track via requestAnimationFrame.

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-06">
  <div class="plx-06__driver" id="plx06-driver">
    <div class="plx-06__sticky">

      <div class="plx-06__bar">
        <span class="plx-06__logo">Horizontal·PLX</span>
        <div class="plx-06__progress-track">
          <div class="plx-06__progress-fill" id="plx06-fill"></div>
        </div>
        <span class="plx-06__counter" id="plx06-counter">01 / 05</span>
      </div>

      <div class="plx-06__viewport">
        <div class="plx-06__track" id="plx06-track">

          <!-- Panel 1 -->
          <div class="plx-06__panel">
            <div class="plx-06__bg" data-plx-bg="-0.3"></div>
            <div class="plx-06__num-layer" data-plx-num="0.6">
              <span class="plx-06__big-num">01</span>
            </div>
            <div class="plx-06__shape-layer" data-plx-shape="0.2">
              <svg class="plx-06__shape-svg" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
                <circle cx="200" cy="200" r="180" stroke="#c084fc" stroke-width="1"/>
                <circle cx="200" cy="200" r="120" stroke="#c084fc" stroke-width="0.5"/>
                <circle cx="200" cy="200" r="60" stroke="#c084fc" stroke-width="0.5"/>
                <line x1="20" y1="200" x2="380" y2="200" stroke="#c084fc" stroke-width="0.5"/>
                <line x1="200" y1="20" x2="200" y2="380" stroke="#c084fc" stroke-width="0.5"/>
                <line x1="73" y1="73" x2="327" y2="327" stroke="#c084fc" stroke-width="0.3"/>
                <line x1="327" y1="73" x2="73" y2="327" stroke="#c084fc" stroke-width="0.3"/>
              </svg>
            </div>
            <div class="plx-06__content" data-plx-content="-0.1">
              <span class="plx-06__panel-tag">Chapter One</span>
              <h2 class="plx-06__panel-title">Motion<br>Through<br>Space</h2>
              <p class="plx-06__panel-body">Vertical scroll drives horizontal travel. Each layer moves at its own speed — the background drifts, the number floats, the content holds firm. Depth from pure math.</p>
              <span class="plx-06__panel-num">01 / 05</span>
            </div>
            <div class="plx-06__hint is-visible" id="plx06-hint">
              <span>Scroll to travel</span>
              <div class="plx-06__hint-arr">
                <span></span><span></span><span></span>
              </div>
            </div>
          </div>

          <!-- Panel 2 -->
          <div class="plx-06__panel">
            <div class="plx-06__bg" data-plx-bg="-0.3"></div>
            <div class="plx-06__num-layer" data-plx-num="0.6">
              <span class="plx-06__big-num">02</span>
            </div>
            <div class="plx-06__shape-layer" data-plx-shape="0.2">
              <svg class="plx-06__shape-svg" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
                <rect x="40" y="40" width="320" height="320" stroke="#4ade80" stroke-width="1"/>
                <rect x="100" y="100" width="200" height="200" stroke="#4ade80" stroke-width="0.5" transform="rotate(15,200,200)"/>
                <rect x="140" y="140" width="120" height="120" stroke="#4ade80" stroke-width="0.5" transform="rotate(30,200,200)"/>
                <line x1="40" y1="40" x2="360" y2="360" stroke="#4ade80" stroke-width="0.3"/>
                <line x1="360" y1="40" x2="40" y2="360" stroke="#4ade80" stroke-width="0.3"/>
              </svg>
            </div>
            <div class="plx-06__content" data-plx-content="-0.1">
              <span class="plx-06__panel-tag">Chapter Two</span>
              <h2 class="plx-06__panel-title">Layers<br>in<br>Tension</h2>
              <p class="plx-06__panel-body">The background moves opposite to your scroll direction. The foreground barely shifts. The gap between them is where depth lives — created without a single CSS 3D property.</p>
              <span class="plx-06__panel-num">02 / 05</span>
            </div>
          </div>

          <!-- Panel 3 -->
          <div class="plx-06__panel">
            <div class="plx-06__bg" data-plx-bg="-0.3"></div>
            <div class="plx-06__num-layer" data-plx-num="0.6">
              <span class="plx-06__big-num">03</span>
            </div>
            <div class="plx-06__shape-layer" data-plx-shape="0.2">
              <svg class="plx-06__shape-svg" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
                <polygon points="200,30 370,310 30,310" stroke="#fb923c" stroke-width="1" fill="none"/>
                <polygon points="200,80 320,290 80,290" stroke="#fb923c" stroke-width="0.5" fill="none"/>
                <polygon points="200,130 270,270 130,270" stroke="#fb923c" stroke-width="0.5" fill="none"/>
                <circle cx="200" cy="200" r="30" stroke="#fb923c" stroke-width="0.5"/>
              </svg>
            </div>
            <div class="plx-06__content" data-plx-content="-0.1">
              <span class="plx-06__panel-tag">Chapter Three</span>
              <h2 class="plx-06__panel-title">Speed<br>as<br>Hierarchy</h2>
              <p class="plx-06__panel-body">Fast-moving elements feel close. Slow-moving elements feel distant. The eye reads speed differentials as spatial distance — an ancient visual instinct made digital.</p>
              <span class="plx-06__panel-num">03 / 05</span>
            </div>
          </div>

          <!-- Panel 4 -->
          <div class="plx-06__panel">
            <div class="plx-06__bg" data-plx-bg="-0.3"></div>
            <div class="plx-06__num-layer" data-plx-num="0.6">
              <span class="plx-06__big-num">04</span>
            </div>
            <div class="plx-06__shape-layer" data-plx-shape="0.2">
              <svg class="plx-06__shape-svg" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
                <ellipse cx="200" cy="200" rx="180" ry="80" stroke="#38bdf8" stroke-width="1"/>
                <ellipse cx="200" cy="200" rx="130" ry="55" stroke="#38bdf8" stroke-width="0.5"/>
                <ellipse cx="200" cy="200" rx="80" ry="30" stroke="#38bdf8" stroke-width="0.5"/>
                <line x1="20" y1="200" x2="380" y2="200" stroke="#38bdf8" stroke-width="0.4"/>
                <circle cx="200" cy="200" r="8" fill="#38bdf8" opacity="0.4"/>
              </svg>
            </div>
            <div class="plx-06__content" data-plx-content="-0.1">
              <span class="plx-06__panel-tag">Chapter Four</span>
              <h2 class="plx-06__panel-title">Four<br>Kinds<br>of Still</h2>
              <p class="plx-06__panel-body">Every layer is in motion — yet nothing feels chaotic. The geometry holds its orbit. The content waits. The number recedes. Stillness is a composition, not an absence.</p>
              <span class="plx-06__panel-num">04 / 05</span>
            </div>
          </div>

          <!-- Panel 5 -->
          <div class="plx-06__panel">
            <div class="plx-06__bg" data-plx-bg="-0.3"></div>
            <div class="plx-06__num-layer" data-plx-num="0.6">
              <span class="plx-06__big-num">05</span>
            </div>
            <div class="plx-06__shape-layer" data-plx-shape="0.2">
              <svg class="plx-06__shape-svg" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M 200 30 L 370 150 L 300 340 L 100 340 L 30 150 Z" stroke="#f472b6" stroke-width="1" fill="none"/>
                <path d="M 200 80 L 320 175 L 265 320 L 135 320 L 80 175 Z" stroke="#f472b6" stroke-width="0.5" fill="none"/>
                <circle cx="200" cy="200" r="40" stroke="#f472b6" stroke-width="0.5"/>
                <circle cx="200" cy="200" r="8" fill="#f472b6" opacity="0.3"/>
              </svg>
            </div>
            <div class="plx-06__content" data-plx-content="-0.1">
              <span class="plx-06__panel-tag">Chapter Five</span>
              <h2 class="plx-06__panel-title">The<br>End<br>Begins</h2>
              <p class="plx-06__panel-body">Five panels, five velocities, one scroll gesture. The journey across them felt longer than the distance traveled — that's what parallax does. It stretches space with motion.</p>
              <span class="plx-06__panel-num">05 / 05</span>
            </div>
          </div>

        </div><!-- /track -->

        <!-- dot nav -->
        <div class="plx-06__dots" id="plx06-dots">
          <div class="plx-06__dot is-active"></div>
          <div class="plx-06__dot"></div>
          <div class="plx-06__dot"></div>
          <div class="plx-06__dot"></div>
          <div class="plx-06__dot"></div>
        </div>

      </div><!-- /viewport -->
    </div><!-- /sticky -->
  </div><!-- /driver -->
</div>
.plx-06, .plx-06 *, .plx-06 *::before, .plx-06 *::after {
  box-sizing: border-box; margin: 0; padding: 0;
}
.plx-06 {
  font-family: 'Space Grotesk', sans-serif;
  background: #06060e;
  color: #f0eff8;
}

/* Tall enough to drive 5 panels of horizontal scroll */
.plx-06__driver {
  height: 500vh;
  position: relative;
}

/* Sticky container holds the whole horizontal experience */
.plx-06__sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* Top bar */
.plx-06__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px 40px;
  flex-shrink: 0;
  border-bottom: 1px solid rgba(240,239,248,0.08);
  position: relative;
  z-index: 10;
}
.plx-06__logo {
  font-family: 'Space Mono', monospace;
  font-size: 12px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  opacity: 0.6;
}
.plx-06__progress-track {
  flex: 1; max-width: 280px; margin: 0 32px;
  height: 1px; background: rgba(255,255,255,0.1); position: relative;
}
.plx-06__progress-fill {
  position: absolute; inset: 0 auto 0 0;
  height: 100%; width: 0%; background: #fff; transition: width 60ms linear;
}
.plx-06__counter {
  font-family: 'Space Mono', monospace;
  font-size: 11px; opacity: 0.35; letter-spacing: 0.1em;
}

/* Horizontal viewport — clips the track */
.plx-06__viewport {
  flex: 1;
  position: relative;
  overflow: hidden;
}

/* Track — moves horizontally, 5 panels wide */
.plx-06__track {
  display: flex;
  width: 500%;
  height: 100%;
  will-change: transform;
}

/* Each panel = 1/5 of the track = 100vw */
.plx-06__panel {
  width: 20%;
  height: 100%;
  position: relative;
  overflow: hidden;
  flex-shrink: 0;
}

/* ── Layer A: Full-bleed background ── */
.plx-06__bg {
  position: absolute;
  inset: -15%;
  will-change: transform;
  transition: none;
}
.plx-06__panel:nth-child(1) .plx-06__bg { background: linear-gradient(135deg, #0a001a 0%, #1a0030 60%, #2a0050 100%); }
.plx-06__panel:nth-child(2) .plx-06__bg { background: linear-gradient(135deg, #001a10 0%, #00301a 60%, #004020 100%); }
.plx-06__panel:nth-child(3) .plx-06__bg { background: linear-gradient(135deg, #1a0800 0%, #30100000 0%, #301000 100%); }
.plx-06__panel:nth-child(3) .plx-06__bg { background: linear-gradient(135deg, #1a0a00 0%, #301200 100%); }
.plx-06__panel:nth-child(4) .plx-06__bg { background: linear-gradient(135deg, #00101a 0%, #001c30 100%); }
.plx-06__panel:nth-child(5) .plx-06__bg { background: linear-gradient(135deg, #1a001a 0%, #280028 100%); }

/* ── Layer B: Giant number ── */
.plx-06__num-layer {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  will-change: transform;
  pointer-events: none;
}
.plx-06__big-num {
  font-family: 'Space Mono', monospace;
  font-size: clamp(180px, 35vw, 480px);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.05em;
  opacity: 0.05;
  color: #fff;
  user-select: none;
}

/* ── Layer C: Decorative SVG shape ── */
.plx-06__shape-layer {
  position: absolute;
  inset: 0;
  will-change: transform;
  pointer-events: none;
}
.plx-06__shape-svg {
  position: absolute;
  top: 50%; right: 8%;
  transform: translateY(-50%);
  width: 36vw; height: 36vw;
  max-width: 400px; max-height: 400px;
  opacity: 0.18;
}

/* ── Layer D: Content ── */
.plx-06__content {
  position: absolute;
  top: 50%;
  left: 10%;
  transform: translateY(-50%);
  z-index: 5;
  will-change: transform;
  max-width: 42%;
}
.plx-06__panel-tag {
  font-family: 'Space Mono', monospace;
  font-size: 10px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  opacity: 0.4;
  margin-bottom: 16px;
  display: block;
}
.plx-06__panel-title {
  font-family: 'Space Grotesk', sans-serif;
  font-size: clamp(36px, 6vw, 84px);
  font-weight: 700;
  line-height: 0.95;
  letter-spacing: -0.03em;
  margin-bottom: 20px;
}
.plx-06__panel-body {
  font-size: 14px;
  font-weight: 300;
  line-height: 1.75;
  opacity: 0.5;
  max-width: 340px;
}
.plx-06__panel-num {
  display: block;
  font-family: 'Space Mono', monospace;
  font-size: 10px;
  letter-spacing: 0.2em;
  opacity: 0.25;
  margin-top: 28px;
}

/* Accent colors per panel */
.plx-06__panel:nth-child(1) .plx-06__panel-title { color: #c084fc; }
.plx-06__panel:nth-child(2) .plx-06__panel-title { color: #4ade80; }
.plx-06__panel:nth-child(3) .plx-06__panel-title { color: #fb923c; }
.plx-06__panel:nth-child(4) .plx-06__panel-title { color: #38bdf8; }
.plx-06__panel:nth-child(5) .plx-06__panel-title { color: #f472b6; }

/* Vertical dot nav */
.plx-06__dots {
  position: absolute;
  bottom: 28px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 20;
  display: flex;
  gap: 10px;
}
.plx-06__dot {
  width: 5px; height: 5px;
  border-radius: 50%;
  background: rgba(255,255,255,0.2);
  transition: all 0.3s;
}
.plx-06__dot.is-active {
  background: #fff;
  transform: scale(1.6);
}

/* Scroll hint — only on panel 1 */
.plx-06__hint {
  position: absolute;
  right: 40px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 12px;
  font-family: 'Space Mono', monospace;
  font-size: 9px;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  opacity: 0;
  transition: opacity 0.4s;
}
.plx-06__hint.is-visible { opacity: 0.35; }
.plx-06__hint-arr {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.plx-06__hint-arr span {
  display: block;
  width: 18px;
  height: 1px;
  background: rgba(255,255,255,0.6);
  animation: plx-06-arr 1.6s ease-in-out infinite;
}
.plx-06__hint-arr span:nth-child(2) { animation-delay: 0.15s; }
.plx-06__hint-arr span:nth-child(3) { animation-delay: 0.3s; }
@keyframes plx-06-arr {
  0%, 100% { transform: scaleX(1); opacity: 0.6; }
  50% { transform: scaleX(1.4); opacity: 1; }
}

@media (max-width: 768px) {
  .plx-06__content { max-width: 70%; left: 6%; }
  .plx-06__shape-svg { display: none; }
  .plx-06__bar { padding: 14px 20px; }
  .plx-06__progress-track { max-width: 140px; margin: 0 16px; }
}

@media (prefers-reduced-motion: reduce) {
  .plx-06__bg, .plx-06__num-layer, .plx-06__shape-layer, .plx-06__content { transform: none !important; }
  .plx-06__track { transform: none !important; }
  .plx-06__hint-arr span { animation: none; }
}
(() => {
  const driver = document.getElementById('plx06-driver');
  const track = document.getElementById('plx06-track');
  const fill = document.getElementById('plx06-fill');
  const counter = document.getElementById('plx06-counter');
  const hint = document.getElementById('plx06-hint');
  const dots = Array.from(document.querySelectorAll('.plx-06__dot'));
  if (!driver || !track) return;

  const PANELS = 5;

  // Per-panel parallax layers
  const panels = Array.from(track.querySelectorAll('.plx-06__panel')).map((panel, i) => ({
    bg: panel.querySelector('[data-plx-bg]'),
    num: panel.querySelector('[data-plx-num]'),
    shape: panel.querySelector('[data-plx-shape]'),
    content: panel.querySelector('[data-plx-content]'),
    bgSpeed: parseFloat(panel.querySelector('[data-plx-bg]')?.dataset.plxBg || 0),
    numSpeed: parseFloat(panel.querySelector('[data-plx-num]')?.dataset.plxNum || 0),
    shapeSpeed: parseFloat(panel.querySelector('[data-plx-shape]')?.dataset.plxShape || 0),
    contentSpeed: parseFloat(panel.querySelector('[data-plx-content]')?.dataset.plxContent || 0),
  }));

  let ticking = false;
  function onScroll() {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => {
      const driverRect = driver.getBoundingClientRect();
      const driverH = driver.offsetHeight;
      const vh = window.innerHeight;
      const vw = window.innerWidth;

      // scrollable range: driverH - vh
      const scrollableH = driverH - vh;
      const rawProgress = Math.max(0, Math.min(1, -driverRect.top / scrollableH));

      // Horizontal offset of the track
      const maxOffset = vw * (PANELS - 1);
      const trackX = rawProgress * maxOffset;
      track.style.transform = `translateX(${-trackX}px)`;

      // Progress bar and counter
      fill.style.width = (rawProgress * 100) + '%';
      const activePanel = Math.min(PANELS - 1, Math.floor(rawProgress * PANELS));
      counter.textContent = String(activePanel + 1).padStart(2,'0') + ' / 0' + PANELS;
      dots.forEach((d, i) => d.classList.toggle('is-active', i === activePanel));

      // Hide hint after first panel
      if (hint) hint.classList.toggle('is-visible', rawProgress < 0.05);

      // Per-layer parallax within each panel
      // The local offset of each panel center relative to the viewport center
      panels.forEach((p, i) => {
        const panelCenter = i * vw;           // absolute X center of panel in track
        const viewCenter = trackX;            // current left edge of visible area
        const localOffset = panelCenter - viewCenter; // px from left edge
        // "distance from center" — 0 when panel is fully visible
        const fromCenter = localOffset - vw / 2;

        if (p.bg) p.bg.style.transform = `translateX(${fromCenter * p.bgSpeed}px)`;
        if (p.num) p.num.style.transform = `translateX(${fromCenter * p.numSpeed}px)`;
        if (p.shape) p.shape.style.transform = `translateX(${fromCenter * p.shapeSpeed}px)`;
        if (p.content) p.content.style.transform = `translateY(-50%) translateX(${fromCenter * p.contentSpeed}px)`;
      });

      ticking = false;
    });
  }

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

Search CodeFronts

Loading…