Back to CSS Stacked Cards Scroll-Activated Stacked Cards (Sticky Stack) CSS + JS
Share
HTML
<div class="scd-stick">
  <div class="scd-stick__rail">
    <span class="scd-stick__pip scd-stick__pip--on"></span><span class="scd-stick__pip"></span><span class="scd-stick__pip"></span><span class="scd-stick__pip"></span>
  </div>
  <div class="scd-stick__scroll-wrap">
    <section class="scd-stick__step scd-stick__step--s1"><div class="scd-stick__grad"></div><div class="scd-stick__sheen"></div>
      <div class="scd-stick__top"><span class="scd-stick__idx">01</span><span class="scd-stick__chip">Phase one</span></div>
      <div><h2 class="scd-stick__h2">Discover</h2><p class="scd-stick__p">We map your goals and audit exactly where you stand today.</p><div class="scd-stick__bar"><i></i></div></div></section>
    <section class="scd-stick__step scd-stick__step--s2"><div class="scd-stick__grad"></div><div class="scd-stick__sheen"></div>
      <div class="scd-stick__top"><span class="scd-stick__idx">02</span><span class="scd-stick__chip">Phase two</span></div>
      <div><h2 class="scd-stick__h2">Design</h2><p class="scd-stick__p">Concepts take shape through rapid, collaborative iteration.</p><div class="scd-stick__bar"><i></i></div></div></section>
    <section class="scd-stick__step scd-stick__step--s3"><div class="scd-stick__grad"></div><div class="scd-stick__sheen"></div>
      <div class="scd-stick__top"><span class="scd-stick__idx">03</span><span class="scd-stick__chip">Phase three</span></div>
      <div><h2 class="scd-stick__h2">Build</h2><p class="scd-stick__p">Production-ready code, tested and shipped with real care.</p><div class="scd-stick__bar"><i></i></div></div></section>
    <section class="scd-stick__step scd-stick__step--s4"><div class="scd-stick__grad"></div><div class="scd-stick__sheen"></div>
      <div class="scd-stick__top"><span class="scd-stick__idx">04</span><span class="scd-stick__chip">Phase four</span></div>
      <div><h2 class="scd-stick__h2">Launch</h2><p class="scd-stick__p">We go live, measure everything, then refine what works.</p><div class="scd-stick__bar"><i></i></div></div></section>
  </div>
</div>
CSS
@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@300;400;500;600;700&display=swap');

.scd-stick, .scd-stick *, .scd-stick *::before, .scd-stick *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* Page-scroll demo — mirrors the original source body. The visitor's
   own page (or the playground iframe) provides the scroll; we do NOT
   create an internal overflow:auto scroller. */
.scd-stick {
  position: relative;
  min-height: 100vh;
  background: #05060a;
  font-family: 'Geist', sans-serif;
  color: #fff;
  scroll-behavior: smooth;
}
.scd-stick::before {
  content: '';
  position: fixed; inset: -30%; z-index: 0;
  background:
    radial-gradient(40% 40% at 25% 25%, rgba(99,102,241,.30), transparent 70%),
    radial-gradient(45% 45% at 75% 30%, rgba(236,72,153,.26), transparent 70%),
    radial-gradient(50% 50% at 50% 80%, rgba(45,212,191,.24), transparent 70%);
  filter: blur(30px);
  animation: scd-stick-drift 20s ease-in-out infinite alternate;
  pointer-events: none;
}
@keyframes scd-stick-drift {
  0% { transform: translate(-3%,-2%) scale(1); }
  100% { transform: translate(4%,3%) scale(1.1); }
}
.scd-stick::after {
  content: '';
  position: fixed; inset: 0; z-index: 0;
  background: repeating-linear-gradient(0deg, rgba(255,255,255,.02) 0 1px, transparent 1px 3px);
  mix-blend-mode: overlay;
  pointer-events: none;
}

.scd-stick__scroll-wrap {
  position: relative;
  z-index: 1;
  width: min(460px, 92vw);
  margin: 0 auto;
  /* vh-based padding matches the source — generous space above + below
     the deck so each card has room to pin without crowding the viewport
     edges. */
  padding: 55vh 0 75vh;
}

.scd-stick__rail {
  position: fixed; top: 50%; right: 24px;
  transform: translateY(-50%);
  z-index: 5;
  display: flex; flex-direction: column; gap: 14px;
}
.scd-stick__pip { width: 8px; height: 8px; border-radius: 50%; background: rgba(255,255,255,.25); transition: all .4s; display: block; }
.scd-stick__pip--on { background: #fff; height: 26px; border-radius: 6px; box-shadow: 0 0 14px rgba(255,255,255,.6); }

.scd-stick__step {
  position: sticky;
  top: 20vh;
  height: 320px;
  border-radius: 28px;
  padding: 38px;
  margin-bottom: 36px;
  display: flex; flex-direction: column; justify-content: space-between;
  overflow: hidden;
  transform-origin: 50% 0;
  box-shadow: 0 40px 80px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.08) inset;
  backdrop-filter: blur(6px);
}
.scd-stick__grad {
  position: absolute; inset: 0; z-index: -1;
  opacity: .95;
  background-size: 200% 200%;
  animation: scd-stick-flow 9s ease infinite;
}
@keyframes scd-stick-flow {
  0% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}
.scd-stick__sheen {
  position: absolute; top: -50%; left: -30%;
  width: 50%; height: 200%;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.35), transparent);
  transform: rotate(18deg);
  animation: scd-stick-sweep 6s ease-in-out infinite;
}
@keyframes scd-stick-sweep {
  0%, 100% { transform: rotate(18deg) translateX(-300px); }
  50% { transform: rotate(18deg) translateX(700px); }
}

.scd-stick__top { display: flex; align-items: center; gap: 14px; }
.scd-stick__idx {
  font-family: 'Instrument Serif', serif; font-style: italic;
  font-size: 4rem; line-height: .8; opacity: .9;
  text-shadow: 0 4px 20px rgba(0,0,0,.3);
}
.scd-stick__chip {
  margin-left: auto;
  font-size: .62rem; letter-spacing: .22em; text-transform: uppercase;
  padding: 7px 14px; border-radius: 999px;
  background: rgba(255,255,255,.18);
  border: 1px solid rgba(255,255,255,.25);
  backdrop-filter: blur(6px);
}
.scd-stick__h2 { font-family: 'Instrument Serif', serif; font-size: 2.4rem; letter-spacing: -.01em; }
.scd-stick__p { opacity: .88; max-width: 32ch; line-height: 1.55; font-weight: 300; }
.scd-stick__bar { height: 4px; border-radius: 4px; background: rgba(255,255,255,.25); overflow: hidden; margin-top: 16px; }
.scd-stick__bar i { display: block; height: 100%; width: 0; background: #fff; border-radius: 4px; transition: width .5s; }

.scd-stick__step--s1 { top: 20vh; }
.scd-stick__step--s1 .scd-stick__grad { background: linear-gradient(135deg,#1e1b4b,#4338ca 45%,#7c3aed); }
.scd-stick__step--s2 { top: 22vh; }
.scd-stick__step--s2 .scd-stick__grad { background: linear-gradient(135deg,#831843,#be185d 45%,#fb7185); }
.scd-stick__step--s3 { top: 24vh; }
.scd-stick__step--s3 .scd-stick__grad { background: linear-gradient(135deg,#064e3b,#0d9488 50%,#34d399); }
.scd-stick__step--s4 { top: 26vh; }
.scd-stick__step--s4 .scd-stick__grad { background: linear-gradient(135deg,#7c2d12,#ea580c 45%,#fbbf24); }

@media (prefers-reduced-motion: reduce) {
  .scd-stick::before,
  .scd-stick__grad,
  .scd-stick__sheen,
  .scd-stick__step,
  .scd-stick__bar i,
  .scd-stick__pip { animation: none !important; transition: none !important; }
}
JS
(() => {
  const root = document.querySelector('.scd-stick');
  if (!root) return;
  const steps = [...root.querySelectorAll('.scd-stick__step')];
  const pips = [...root.querySelectorAll('.scd-stick__pip')];
  if (!steps.length) return;

  function update() {
    let active = 0;
    steps.forEach((s, i) => {
      const r = s.getBoundingClientRect();
      const stickTop = window.innerHeight * (0.20 + i * 0.02);
      const next = steps[i + 1];
      if (next) {
        const nr = next.getBoundingClientRect();
        const prog = Math.min(Math.max((stickTop + 320 - nr.top) / 320, 0), 1);
        s.style.transform = `scale(${1 - prog * 0.10}) translateY(${prog * -8}px)`;
        s.style.filter = `brightness(${1 - prog * 0.35})`;
        const fill = s.querySelector('.scd-stick__bar i');
        if (fill) fill.style.width = (prog * 100) + '%';
      } else {
        const fill = s.querySelector('.scd-stick__bar i');
        if (fill) fill.style.width = '100%';
      }
      if (r.top <= stickTop + 10) active = i;
    });
    pips.forEach((p, i) => p.classList.toggle('scd-stick__pip--on', i === active));
  }

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