web design
COMPONENT L / STACKING CARDS · STICKY LOCK + FADE

Five movements, one relentless loop.

Research, build, validate, deploy, compound.

I

Research

Build a thesis from evidence.

II

Build

Code the strategy.

III

Validate

Backtest on real data.

IV

Deploy

Send it live.

V

Compound

Iterate and accrue.

GUIDE / STACKING CARDS

Overview

Sticky cards lock over each other as the next one rises. The buried card eases down in scale and opacity so a thin peek of each top edge (the gold hairline) remains visible beneath the card above. Scroll-scrubbed, so there is no replay button; scroll back up and the stack unspools.

When to use

A sequential process or loop with 3 to 6 steps, where each step carries equal weight and the order is the story. Good for methodologies, pipelines, and loops that compound on themselves.

Spec

Tokens
surface card, 1px line, gold hairline top, big-shadow
Sticky
top 6.5rem + i*1.5rem (i = 0-based)
Motion
scale 0.92 / opacity 0.45, scrub top 70% to 22%
Origin
transformOrigin center top
A11y
reduced-motion collapses to a plain static list
Radius
1.5rem (rounded-3xl)

Anti-patterns

Don't use for non-sequential content where order is arbitrary. Don't exceed about 6 cards or the buried stack reads as visual noise and the scroll length becomes a chore.

<!-- Requires GSAP: https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/gsap.min.js -->

<style>
:root{
  --bg:#0b0a08;--surface:#14110b;--ivory:#efe8d6;--muted:#9c9078;--faint:#5a5446;
  --line:#2a2519;--line-faint:rgba(239,232,214,.08);--gold:#c9a24b;--gold-soft:#e7cd8c;
  --font-display:'Instrument Serif',ui-serif,Georgia,'Times New Roman',serif;
  --font-title:'Libre Caslon Condensed',ui-serif,Georgia,'Times New Roman',serif;
  --font-body:'Inter',system-ui,-apple-system,sans-serif;
  --font-mono:'JetBrains Mono',ui-monospace,monospace;
  --ease:cubic-bezier(.22,1,.36,1);--dur-fast:200ms;--dur-base:400ms;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--ivory);font-family:var(--font-body);min-height:100vh;padding:64px 48px}
@keyframes hf-shimmer{0%{background-position:175% 0}100%{background-position:-75% 0}}
.text-shimmer{background-image:linear-gradient(100deg,var(--muted) 0%,var(--muted) 42%,var(--gold-soft) 48%,#f7eed1 50%,var(--gold-soft) 52%,var(--muted) 58%,var(--muted) 100%);background-size:250% 100%;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent;animation:hf-shimmer 13s ease-in-out infinite}
@media(prefers-reduced-motion:reduce){.text-shimmer{animation:none;-webkit-text-fill-color:var(--muted);color:var(--muted)}}
.intro{max-width:960px;margin-bottom:72px}
.intro h2{font-family:var(--font-title);font-size:clamp(2.25rem,5.5vw,3.75rem);line-height:1.04;color:var(--ivory);font-weight:400;letter-spacing:.01em}
.intro h2 em{font-style:normal;color:var(--gold-soft)}
.intro p{margin-top:20px;max-width:36rem;font-weight:300;line-height:1.7;font-size:1.0625rem}
.stack{max-width:960px;margin:0 auto;display:flex;flex-direction:column}
.card-wrap{position:sticky;top:6.5rem}
.card{position:relative;display:flex;flex-direction:column;justify-content:center;min-height:220px;border:1px solid var(--line);background:var(--surface);border-radius:1.5rem;padding:32px 24px;overflow:hidden;box-shadow:0 -1px 0 0 rgba(201,162,75,.12),0 40px 90px -50px rgba(0,0,0,.85)}
.card::before{content:"";position:absolute;left:0;right:0;top:0;height:1px;background:linear-gradient(90deg,transparent,rgba(201,162,75,.4),transparent)}
.card-inner{display:flex;flex-direction:column;gap:20px}
.numeral{font-family:var(--font-title);font-size:4.5rem;line-height:1;color:var(--gold-soft);font-weight:400}
.card-body{max-width:36rem}
.card-body h3{font-family:var(--font-title);font-size:2rem;color:var(--ivory);font-weight:400;line-height:1.05}
.card-body p{margin-top:12px;font-family:var(--font-body);font-size:1rem;font-weight:300;line-height:1.7;color:var(--muted)}
@media(min-width:768px){
  .card{min-height:260px;padding:48px 40px}
  .card-inner{flex-direction:row;align-items:center;gap:56px}
  .numeral{font-size:7rem}
  .card-body h3{font-size:3rem}
  .card-body p{font-size:1.0625rem}
}
</style>

<header class="intro">
  <h2>Five movements, one <em>relentless</em> loop.</h2>
  <p class="text-shimmer">Research, build, validate, deploy, compound.</p>
</header>

<main class="stack" id="stack">
  <div class="card-wrap" style="top:calc(6.5rem + 0rem)">
    <article class="card">
      <div class="card-inner">
        <span class="numeral">I</span>
        <div class="card-body">
          <h3>Research</h3>
          <p>Build a thesis from evidence.</p>
        </div>
      </div>
    </article>
  </div>
  <div class="card-wrap" style="top:calc(6.5rem + 1.5rem)">
    <article class="card">
      <div class="card-inner">
        <span class="numeral">II</span>
        <div class="card-body">
          <h3>Build</h3>
          <p>Code the strategy.</p>
        </div>
      </div>
    </article>
  </div>
  <div class="card-wrap" style="top:calc(6.5rem + 3rem)">
    <article class="card">
      <div class="card-inner">
        <span class="numeral">III</span>
        <div class="card-body">
          <h3>Validate</h3>
          <p>Backtest on real data.</p>
        </div>
      </div>
    </article>
  </div>
  <div class="card-wrap" style="top:calc(6.5rem + 4.5rem)">
    <article class="card">
      <div class="card-inner">
        <span class="numeral">IV</span>
        <div class="card-body">
          <h3>Deploy</h3>
          <p>Send it live.</p>
        </div>
      </div>
    </article>
  </div>
  <div class="card-wrap" style="top:calc(6.5rem + 6rem)">
    <article class="card">
      <div class="card-inner">
        <span class="numeral">V</span>
        <div class="card-body">
          <h3>Compound</h3>
          <p>Iterate and accrue.</p>
        </div>
      </div>
    </article>
  </div>
</main>

<script>
gsap.registerPlugin(ScrollTrigger);
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
  const wrappers = gsap.utils.toArray('.card-wrap');
  const cards = gsap.utils.toArray('.card');
  cards.forEach((card, i) => {
    if (i >= cards.length - 1) return;
    gsap.to(card, {
      scale: 0.92,
      opacity: 0.45,
      ease: 'none',
      transformOrigin: 'center top',
      scrollTrigger: {
        trigger: wrappers[i + 1],
        start: 'top 70%',
        end: 'top 22%',
        scrub: true,
      },
    });
  });
});
mm.add("(prefers-reduced-motion: reduce)", () => {
  // plain vertical list: no sticky, no scale/opacity tween
  gsap.set('.card-wrap', { position: 'static', marginBottom: 24 });
});
</script>