Tailwind UI Pattern Registry for humans and agents

stagger grid list gsap scroll-reveal gsap

Stagger Children

Stagger Children

Child elements animate into view one after another when their parent container enters the viewport.

GSAP Scroll
Live Preview

Dependencies

GSAP v3.12.5 ScrollTrigger v3.12.5
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Stagger Children — Motion Recipe</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
  <style>
    @media (prefers-reduced-motion: reduce) {
      .feature-item, #stagger-list li { opacity: 1 !important; transform: none !important; }
    }
  </style>
</head>
<body class="bg-slate-900 text-slate-200 p-8 font-sans">

  <!-- Hero -->
  <div class="mx-auto mb-32 max-w-3xl pt-16 text-center">
    <span class="mb-6 inline-block rounded-full border border-emerald-500/20 bg-emerald-500/10 px-3.5 py-1 text-[0.7rem] font-semibold uppercase tracking-widest text-emerald-400">Stagger Reveal</span>
    <h1 class="mb-4 text-5xl font-black leading-tight text-slate-100">Children animate<br>one after another</h1>
    <p class="mx-auto max-w-sm text-base leading-relaxed text-slate-500">Scroll down to see grids and lists stagger into view when their container enters the viewport.</p>
  </div>

  <!-- Section heading: Feature grid -->
  <div class="mx-auto mb-8 max-w-3xl">
    <p class="mb-1 text-[0.7rem] font-semibold uppercase tracking-[0.14em] text-slate-500">Feature grid</p>
    <h2 class="text-2xl font-black leading-tight text-slate-100">Six capabilities, one trigger</h2>
  </div>

  <!-- Feature grid -->
  <div id="feature-grid" class="mx-auto mb-24 max-w-3xl grid grid-cols-1 sm:grid-cols-3 gap-px bg-white/[0.06] border border-white/[0.06] rounded-2xl overflow-hidden">
    <div class="feature-item bg-slate-900 p-7">
      <div class="mb-3 text-[2rem] font-black text-indigo-500/30 tabular-nums">01</div>
      <p class="mb-1 text-sm font-semibold text-slate-100">Scroll-driven</p>
      <p class="text-xs leading-relaxed text-slate-500">The parent container is the ScrollTrigger. All children share the same entry point.</p>
    </div>
    <div class="feature-item bg-slate-900 p-7">
      <div class="mb-3 text-[2rem] font-black text-indigo-500/30 tabular-nums">02</div>
      <p class="mb-1 text-sm font-semibold text-slate-100">Stagger delay</p>
      <p class="text-xs leading-relaxed text-slate-500">Each child starts 0.08s after the previous. Feels natural, never mechanical.</p>
    </div>
    <div class="feature-item bg-slate-900 p-7">
      <div class="mb-3 text-[2rem] font-black text-indigo-500/30 tabular-nums">03</div>
      <p class="mb-1 text-sm font-semibold text-slate-100">From-state only</p>
      <p class="text-xs leading-relaxed text-slate-500">GSAP animates from the defined state to the element's natural CSS position.</p>
    </div>
    <div class="feature-item bg-slate-900 p-7">
      <div class="mb-3 text-[2rem] font-black text-indigo-500/30 tabular-nums">04</div>
      <p class="mb-1 text-sm font-semibold text-slate-100">Any children</p>
      <p class="text-xs leading-relaxed text-slate-500">Works on cards, list items, table rows, avatar stacks — any repeated element.</p>
    </div>
    <div class="feature-item bg-slate-900 p-7">
      <div class="mb-3 text-[2rem] font-black text-indigo-500/30 tabular-nums">05</div>
      <p class="mb-1 text-sm font-semibold text-slate-100">One GSAP call</p>
      <p class="text-xs leading-relaxed text-slate-500">gsap.from('.child', { stagger, scrollTrigger: { trigger: '.parent' } })</p>
    </div>
    <div class="feature-item bg-slate-900 p-7">
      <div class="mb-3 text-[2rem] font-black text-indigo-500/30 tabular-nums">06</div>
      <p class="mb-1 text-sm font-semibold text-slate-100">Plays once</p>
      <p class="text-xs leading-relaxed text-slate-500">toggleActions: "play none none none" — fires on enter, never reverses.</p>
    </div>
  </div>

  <!-- Section heading: List -->
  <div class="mx-auto mb-8 max-w-3xl">
    <p class="mb-1 text-[0.7rem] font-semibold uppercase tracking-[0.14em] text-slate-500">List example</p>
    <h2 class="text-2xl font-black leading-tight text-slate-100">Items arrive in sequence</h2>
  </div>

  <!-- Stagger list -->
  <ul id="stagger-list" class="mx-auto mb-24 max-w-3xl flex flex-col gap-3 list-none">
    <li class="flex items-center gap-3 rounded-xl border border-white/[0.06] bg-white/[0.03] px-5 py-4">
      <span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-400">
        <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
      </span>
      <span class="text-sm font-medium text-slate-200">Scroll to trigger point</span>
      <span class="ml-auto text-xs text-slate-500">Step 1</span>
    </li>
    <li class="flex items-center gap-3 rounded-xl border border-white/[0.06] bg-white/[0.03] px-5 py-4">
      <span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-400">
        <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
      </span>
      <span class="text-sm font-medium text-slate-200">ScrollTrigger fires on parent enter</span>
      <span class="ml-auto text-xs text-slate-500">Step 2</span>
    </li>
    <li class="flex items-center gap-3 rounded-xl border border-white/[0.06] bg-white/[0.03] px-5 py-4">
      <span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-400">
        <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
      </span>
      <span class="text-sm font-medium text-slate-200">First child animates immediately</span>
      <span class="ml-auto text-xs text-slate-500">Step 3</span>
    </li>
    <li class="flex items-center gap-3 rounded-xl border border-white/[0.06] bg-white/[0.03] px-5 py-4">
      <span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-400">
        <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
      </span>
      <span class="text-sm font-medium text-slate-200">Each subsequent child waits 80ms</span>
      <span class="ml-auto text-xs text-slate-500">Step 4</span>
    </li>
    <li class="flex items-center gap-3 rounded-xl border border-white/[0.06] bg-white/[0.03] px-5 py-4">
      <span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-400">
        <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
      </span>
      <span class="text-sm font-medium text-slate-200">All children at final position</span>
      <span class="ml-auto text-xs text-slate-500">Step 5</span>
    </li>
  </ul>

  <div class="h-16"></div>

  <script>
    gsap.registerPlugin(ScrollTrigger);

    // Feature grid: stagger children from bottom
    gsap.from('#feature-grid .feature-item', {
      scrollTrigger: {
        trigger: '#feature-grid',
        start: 'top 80%',
        toggleActions: 'play none none none',
      },
      opacity: 0,
      y: 32,
      duration: 0.6,
      ease: 'power2.out',
      stagger: 0.08,
    });

    // List: stagger from left
    gsap.from('#stagger-list li', {
      scrollTrigger: {
        trigger: '#stagger-list',
        start: 'top 80%',
        toggleActions: 'play none none none',
      },
      opacity: 0,
      x: -24,
      duration: 0.5,
      ease: 'power2.out',
      stagger: 0.1,
    });
  </script>
</body>
</html>

The parent container is the ScrollTrigger target — all children share the same entry point. This is different from individual fade-up reveals, where each element has its own trigger. Here, one scroll event kicks off the entire stagger sequence.

The stagger value controls the delay between each child. 0.08s feels natural for grids; 0.1s works well for lists. Going above 0.15s starts to feel mechanical unless you have very few items.

Direction matters: grids look good entering from the bottom (y: 32), while horizontal lists or nav items benefit from a left entry (x: -24).