10 CSS Parallax Effects 09 / 10

CSS Zoom-In / Depth Parallax on Scroll

Cinematic camera-zoom illusion: concentric-ring system scales 1× to 3× while background scales 1× to 2× in the opposite direction as you scroll, building a 'falling forward' sensation.

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-09">

  <!-- SCENE 1: Zoom in -->
  <div class="plx-09__zoom" id="plx09-zoom">
    <div class="plx-09__zoom-sticky">
      <div class="plx-09__zoom-bg"  id="plx09-bg"></div>
      <div class="plx-09__zoom-grid" id="plx09-grid"></div>
      <div class="plx-09__rings" id="plx09-rings">
        <div class="plx-09__ring"></div>
        <div class="plx-09__ring"></div>
        <div class="plx-09__ring"></div>
        <div class="plx-09__ring"></div>
        <div class="plx-09__ring"></div>
        <div class="plx-09__ring"></div>
      </div>
      <div class="plx-09__dot"></div>
      <div class="plx-09__zoom-content" id="plx09-content">
        <span class="plx-09__zoom-eyebrow">Scroll-Driven Zoom · CSS Scale</span>
        <h1 class="plx-09__zoom-h">GET<br>CLOSER</h1>
        <span class="plx-09__zoom-sub" id="plx09-sub">The world expands toward you. A depth illusion built on CSS scale and scroll position — no 3D transforms needed.</span>
      </div>
    </div>
  </div>

  <!-- GALLERY -->
  <div class="plx-09__gallery">
    <div class="plx-09__cell">
      <div class="plx-09__cell-inner" id="plx09-g1"></div>
      <div class="plx-09__cell-label">Nebula<small>Deep space · scroll depth</small></div>
    </div>
    <div class="plx-09__cell">
      <div class="plx-09__cell-inner" id="plx09-g2"></div>
      <div class="plx-09__cell-label">Abyss<small>Ocean floor · scroll depth</small></div>
    </div>
    <div class="plx-09__cell">
      <div class="plx-09__cell-inner" id="plx09-g3"></div>
      <div class="plx-09__cell-label">Ember<small>Core heat · scroll depth</small></div>
    </div>
  </div>

  <!-- OUTRO -->
  <div class="plx-09__outro" id="plx09-outro">
    <div class="plx-09__outro-sticky">
      <div class="plx-09__outro-bg" id="plx09-outro-bg"></div>
      <div class="plx-09__outro-text" id="plx09-outro-text">
        <h2>Depth is a <em>feeling,</em><br>not a fact.</h2>
        <p>Your brain reads scale change as three-dimensional approach. The zoom-in effect triggers that ancient spatial instinct — getting closer to something important.</p>
      </div>
    </div>
  </div>

</div>
.plx-09, .plx-09 *, .plx-09 *::before, .plx-09 *::after {
  box-sizing: border-box; margin: 0; padding: 0;
}
.plx-09 {
  --gold: #d4a843;
  --ink: #080608;
  --off: #f2eee8;
  background: var(--ink);
  color: var(--off);
  font-family: 'Libre Baskerville', serif;
  /* overflow-x:hidden was breaking position:sticky on descendants.
     Setting any overflow axis to non-visible creates a new scroll
     context, making this element the sticky-containment ancestor
     instead of body — so the .plx-09__zoom-sticky inner never
     pinned, and the visitor saw the zoom scene bg + rings scroll
     past as a single block (looked like dead empty scroll between
     sections). overflow:clip is the modern equivalent that
     prevents horizontal overflow WITHOUT establishing a scroll
     context — sticky works correctly underneath. Chrome 90+,
     Safari 16+, Firefox 81+ (all evergreen majors). */
  overflow-x: clip;
}

/* ═══ SCENE 1: Zoom-through ═══ */
.plx-09__zoom {
  position: relative;
  height: 400vh;
}
.plx-09__zoom-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* BG: scales UP (world expanding toward viewer) */
.plx-09__zoom-bg {
  position: absolute;
  inset: -20%;
  will-change: transform;
  transform-origin: center center;
  background:
    radial-gradient(ellipse at 50% 50%, rgba(212,168,67,0.2) 0%, transparent 40%),
    conic-gradient(from 0deg at 50% 50%,
      #0a0808 0deg, #110a10 60deg, #0a0a12 120deg,
      #0c0808 180deg, #100c08 240deg, #080a0c 300deg, #0a0808 360deg);
}

/* Grid: scales UP faster (closer layer) */
.plx-09__zoom-grid {
  position: absolute;
  inset: -50%;
  will-change: transform;
  transform-origin: center center;
  background-image:
    linear-gradient(rgba(212,168,67,0.06) 1px, transparent 1px),
    linear-gradient(90deg, rgba(212,168,67,0.06) 1px, transparent 1px);
  background-size: 80px 80px;
}

/* Rings: scale UP most (fly through them) */
.plx-09__rings {
  position: absolute;
  width: 100vmin; height: 100vmin;
  will-change: transform;
  transform-origin: center center;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
}
.plx-09__ring {
  position: absolute;
  border-radius: 50%;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  border: 1px solid;
}
.plx-09__ring:nth-child(1) { width: 92%; height: 92%; border-color: rgba(212,168,67,0.10); }
.plx-09__ring:nth-child(2) { width: 74%; height: 74%; border-color: rgba(212,168,67,0.14); }
.plx-09__ring:nth-child(3) { width: 56%; height: 56%; border-color: rgba(212,168,67,0.20); }
.plx-09__ring:nth-child(4) { width: 38%; height: 38%; border-color: rgba(212,168,67,0.30); }
.plx-09__ring:nth-child(5) { width: 22%; height: 22%; border-color: rgba(212,168,67,0.45); }
.plx-09__ring:nth-child(6) { width: 8%;  height: 8%;  border-color: rgba(212,168,67,0.7); background: rgba(212,168,67,0.1); }

/* Gold dot center */
.plx-09__dot {
  position: absolute;
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--gold);
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  box-shadow: 0 0 20px rgba(212,168,67,0.9), 0 0 60px rgba(212,168,67,0.4);
  z-index: 4;
}

/* Content: fades and slides in from bottom as zoom progresses */
.plx-09__zoom-content {
  position: relative;
  z-index: 10;
  text-align: center;
  padding: 0 40px;
  opacity: 0;
  transform: translateY(50px);
  will-change: opacity, transform;
  pointer-events: none;
}

.plx-09__zoom-eyebrow {
  font-family: 'Big Shoulders Display', sans-serif;
  font-size: 11px;
  letter-spacing: 0.4em;
  text-transform: uppercase;
  color: var(--gold);
  display: block;
  margin-bottom: 16px;
}
.plx-09__zoom-h {
  font-family: 'Big Shoulders Display', sans-serif;
  font-weight: 900;
  font-size: clamp(56px, 13vw, 170px);
  letter-spacing: -0.04em;
  line-height: 0.88;
}
.plx-09__zoom-sub {
  margin-top: 24px;
  font-style: italic;
  font-size: 17px;
  line-height: 1.7;
  opacity: 0;
  color: rgba(242,238,232,0.55);
  max-width: 420px;
  display: block;
  will-change: opacity;
}

/* ═══ SCENE 2: Gallery — zoom inside bounded cells ═══ */
.plx-09__gallery {
  padding: 100px 40px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
  max-width: 1100px;
  margin: 0 auto;
}

.plx-09__cell {
  position: relative;
  aspect-ratio: 3/4;
  overflow: hidden;
  cursor: default;
}

/* Each cell inner: starts zoomed in (scale 1.3), scrolls to 1.0 as it enters viewport */
.plx-09__cell-inner {
  position: absolute;
  inset: -15%;
  transform-origin: center center;
  will-change: transform;
}
.plx-09__cell:nth-child(1) .plx-09__cell-inner {
  background: radial-gradient(circle at 40% 40%, #4a2870 0%, #140820 70%);
}
.plx-09__cell:nth-child(2) .plx-09__cell-inner {
  background: radial-gradient(circle at 55% 45%, #154060 0%, #04101c 70%);
}
.plx-09__cell:nth-child(3) .plx-09__cell-inner {
  background: radial-gradient(circle at 50% 55%, #5a1a08 0%, #180604 70%);
}
.plx-09__cell-inner::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 40%, rgba(255,255,255,0.1) 0%, transparent 55%);
}

.plx-09__cell-label {
  position: absolute;
  bottom: 20px;
  left: 20px;
  z-index: 3;
  font-family: 'Big Shoulders Display', sans-serif;
  font-size: 32px;
  font-weight: 900;
  letter-spacing: -0.02em;
  text-shadow: 0 2px 16px rgba(0,0,0,0.8);
}
.plx-09__cell-label small {
  display: block;
  font-family: 'Libre Baskerville', serif;
  font-size: 11px;
  font-weight: 400;
  font-style: italic;
  letter-spacing: 0.05em;
  opacity: 0.5;
  margin-top: 4px;
}

/* ═══ SCENE 3: Outro zoom ═══ */
.plx-09__outro {
  position: relative;
  height: 300vh;
}
.plx-09__outro-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.plx-09__outro-bg {
  position: absolute;
  inset: -20%;
  will-change: transform;
  transform-origin: center center;
  background: radial-gradient(ellipse at 50% 50%, #1a0c26 0%, #080608 65%);
}
.plx-09__outro-text {
  position: relative;
  z-index: 5;
  text-align: center;
  will-change: transform;
}
.plx-09__outro-text h2 {
  font-family: 'Big Shoulders Display', sans-serif;
  font-weight: 900;
  font-size: clamp(44px, 10vw, 130px);
  letter-spacing: -0.04em;
  line-height: 0.9;
}
.plx-09__outro-text h2 em {
  font-style: italic;
  font-family: 'Libre Baskerville', serif;
  font-weight: 400;
  color: var(--gold);
  font-size: 0.65em;
}
.plx-09__outro-text p {
  margin-top: 30px;
  font-style: italic;
  font-size: 15px;
  line-height: 1.8;
  color: rgba(242,238,232,0.4);
  max-width: 380px;
  margin-left: auto;
  margin-right: auto;
}

@media (max-width: 768px) {
  .plx-09__gallery { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
  .plx-09__zoom-bg, .plx-09__zoom-grid, .plx-09__rings,
  .plx-09__cell-inner, .plx-09__outro-bg, .plx-09__outro-text { transform: none !important; }
  .plx-09__zoom-content { opacity: 1 !important; transform: none !important; }
  .plx-09__zoom-sub { opacity: 1 !important; }
}
(() => {
  const zoomBlock   = document.getElementById('plx09-zoom');
  const bg          = document.getElementById('plx09-bg');
  const grid        = document.getElementById('plx09-grid');
  const rings       = document.getElementById('plx09-rings');
  const content     = document.getElementById('plx09-content');
  const sub         = document.getElementById('plx09-sub');
  const galCells    = [
    document.getElementById('plx09-g1'),
    document.getElementById('plx09-g2'),
    document.getElementById('plx09-g3'),
  ];
  const outroBlock  = document.getElementById('plx09-outro');
  const outroBg     = document.getElementById('plx09-outro-bg');
  const outroText   = document.getElementById('plx09-outro-text');

  let ticking = false;

  function prog(el) {
    if (!el) return 0;
    const r = el.getBoundingClientRect();
    const h = el.offsetHeight - window.innerHeight;
    return h > 0 ? Math.max(0, Math.min(1, -r.top / h)) : 0;
  }

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

      // ── SCENE 1 ──
      const p = prog(zoomBlock);
      // bg: scales from 1 → 2.2 (world rushes in)
      if (bg) bg.style.transform = `scale(${1 + p * 1.2})`;
      // grid: scales faster (closer layer)
      if (grid) grid.style.transform = `scale(${1 + p * 1.8})`;
      // rings: fly through (scale 1 → 3.5)
      if (rings) rings.style.transform = `translate(-50%,-50%) scale(${1 + p * 2.5})`;

      // content: fade in after 35% scroll, slide up
      const cp = Math.max(0, (p - 0.35) / 0.45);
      if (content) {
        content.style.opacity = String(Math.min(1, cp * 1.5));
        content.style.transform = `translateY(${(1 - Math.min(1, cp)) * 50}px)`;
      }
      if (sub) sub.style.opacity = String(Math.min(1, Math.max(0, (p - 0.55) / 0.3)));

      // ── GALLERY: parallax inside each cell ──
      galCells.forEach((cell) => {
        if (!cell) return;
        const parent = cell.closest('.plx-09__cell');
        if (!parent) return;
        const r = parent.getBoundingClientRect();
        if (r.bottom < -vh || r.top > vh * 2) return;
        const centerOffset = (r.top + r.height / 2) - vh / 2;
        // Cell comes in zoomed out (1.0) from below, scale up as it centers
        const cellProg = Math.max(0, Math.min(1, (vh * 0.6 - r.top) / (vh * 0.8)));
        const scaleVal = 1.0 + cellProg * 0.25;
        const yShift = centerOffset * 0.1;
        cell.style.transform = `scale(${scaleVal}) translateY(${yShift}px)`;
      });

      // ── OUTRO ──
      const op = prog(outroBlock);
      if (outroBg) outroBg.style.transform = `scale(${1 + op * 0.5})`;
      if (outroText) outroText.style.transform = `scale(${1 + op * 0.06}) translateY(${op * -18}px)`;

      ticking = false;
    });
  }

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

Search CodeFronts

Loading…