Tailwind UI Pattern Registry for humans and agents

scroll-reveal fade entrance motion-one inview stagger waapi motion-one

Fade Up (Motion)

Fade Up (Motion)

Elements fade and rise into view using Motion's inView() and stagger() — driven by the Web Animations API, no external runtime.

Motion One Scroll
Live Preview

Dependencies

Motion v11.11.13
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Fade Up (Motion) — Motion Recipe</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://cdn.jsdelivr.net/npm/motion@11.11.13/dist/motion.js"></script>
  <style>
    [data-reveal], [data-stagger] > *, [data-stagger-x] > * {
      opacity: 0;
    }
    @media (prefers-reduced-motion: reduce) {
      [data-reveal], [data-stagger] > *, [data-stagger-x] > * {
        opacity: 1 !important;
      }
    }
  </style>
</head>
<body class="bg-slate-900 text-slate-200 font-sans">

  <!-- Hero -->
  <div class="mx-auto mb-32 max-w-3xl px-8 pt-20 text-center">
    <span class="mb-6 inline-block rounded-full border border-violet-500/20 bg-violet-500/10 px-3.5 py-1 text-[0.7rem] font-semibold uppercase tracking-widest text-violet-400">Motion One</span>
    <h1 class="mb-4 text-5xl font-black leading-tight text-slate-100">Fade up with<br>the Web API</h1>
    <p class="mx-auto mb-8 max-w-sm text-base leading-relaxed text-slate-400">Motion drives animations through the browser's native Web Animations API. Zero runtime cost, hardware-accelerated by default.</p>
    <a href="#" class="inline-block cursor-default rounded-xl bg-violet-600 px-7 py-2.5 text-sm font-semibold text-white">Get started</a>
  </div>

  <!-- Single reveals -->
  <div class="mx-auto max-w-3xl space-y-24 px-8 pb-24">

    <div data-reveal>
      <p class="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">inView()</p>
      <h2 class="mb-4 text-3xl font-black leading-tight text-slate-100">Fires once per element</h2>
      <p class="max-w-[560px] text-base leading-relaxed text-slate-400">Each element with <code class="rounded bg-slate-800 px-1.5 py-0.5 font-mono text-xs text-violet-300">data-reveal</code> gets its own <code class="rounded bg-slate-800 px-1.5 py-0.5 font-mono text-xs text-violet-300">inView()</code> observer. The callback fires once when the element enters the viewport — the animation plays forward and the observer disconnects.</p>
    </div>

    <div class="h-px bg-white/[0.06]"></div>

    <!-- Stagger grid -->
    <div>
      <p class="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">stagger()</p>
      <h2 class="mb-8 text-3xl font-black leading-tight text-slate-100">Grids animate in sequence</h2>
      <div data-stagger class="grid grid-cols-1 gap-px bg-white/[0.06] sm:grid-cols-3 rounded-2xl border border-white/[0.06] overflow-hidden">
        <div class="bg-slate-900 p-7">
          <p class="mb-2 text-xl font-black tabular-nums text-violet-500/40">01</p>
          <p class="mb-1 text-sm font-semibold text-slate-100">Web Animations API</p>
          <p class="text-xs leading-relaxed text-slate-500">Motion uses WAAPI under the hood — browser-native, off main thread, GPU composited.</p>
        </div>
        <div class="bg-slate-900 p-7">
          <p class="mb-2 text-xl font-black tabular-nums text-violet-500/40">02</p>
          <p class="mb-1 text-sm font-semibold text-slate-100">~18 KB total</p>
          <p class="text-xs leading-relaxed text-slate-500">The full animate + inView + scroll + stagger surface weighs less than one GSAP plugin.</p>
        </div>
        <div class="bg-slate-900 p-7">
          <p class="mb-2 text-xl font-black tabular-nums text-violet-500/40">03</p>
          <p class="mb-1 text-sm font-semibold text-slate-100">stagger(0.08)</p>
          <p class="text-xs leading-relaxed text-slate-500">Pass a delay function to the <code class="font-mono text-violet-300">delay</code> option. Each child gets an offset of n × 80ms.</p>
        </div>
        <div class="bg-slate-900 p-7">
          <p class="mb-2 text-xl font-black tabular-nums text-violet-500/40">04</p>
          <p class="mb-1 text-sm font-semibold text-slate-100">From-state in CSS</p>
          <p class="text-xs leading-relaxed text-slate-500">Initial opacity and translate are set in CSS — no invisible flash before JS runs.</p>
        </div>
        <div class="bg-slate-900 p-7">
          <p class="mb-2 text-xl font-black tabular-nums text-violet-500/40">05</p>
          <p class="mb-1 text-sm font-semibold text-slate-100">Any selector</p>
          <p class="text-xs leading-relaxed text-slate-500">Pass a CSS selector string or a NodeList. Works on grids, lists, and table rows.</p>
        </div>
        <div class="bg-slate-900 p-7">
          <p class="mb-2 text-xl font-black tabular-nums text-violet-500/40">06</p>
          <p class="mb-1 text-sm font-semibold text-slate-100">Plays once</p>
          <p class="text-xs leading-relaxed text-slate-500">inView returns an early-exit function. Return it in the callback to auto-disconnect after one play.</p>
        </div>
      </div>
    </div>

    <div class="h-px bg-white/[0.06]"></div>

    <!-- Second reveal -->
    <div data-reveal>
      <p class="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">Easing</p>
      <h2 class="mb-4 text-3xl font-black leading-tight text-slate-100">Spring-like without<br>a spring solver</h2>
      <p class="max-w-[560px] text-base leading-relaxed text-slate-400">The cubic-bezier <code class="rounded bg-slate-800 px-1.5 py-0.5 font-mono text-xs text-violet-300">[0.22, 1, 0.36, 1]</code> decelerates fast and settles softly — a good default for entrance animations that feel physical without an actual spring.</p>
    </div>

    <!-- Stagger list -->
    <div>
      <p class="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-slate-500">List example</p>
      <h2 class="mb-8 text-3xl font-black leading-tight text-slate-100">Items from the left</h2>
      <ul data-stagger-x class="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-7 w-7 shrink-0 items-center justify-center rounded-lg bg-violet-500/10 text-violet-400 text-xs font-bold">1</span>
          <span class="text-sm font-medium text-slate-200">inView() triggers when element enters viewport</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-7 w-7 shrink-0 items-center justify-center rounded-lg bg-violet-500/10 text-violet-400 text-xs font-bold">2</span>
          <span class="text-sm font-medium text-slate-200">animate() applies keyframes via Web Animations API</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-7 w-7 shrink-0 items-center justify-center rounded-lg bg-violet-500/10 text-violet-400 text-xs font-bold">3</span>
          <span class="text-sm font-medium text-slate-200">stagger(0.07) staggers each list item by 70ms</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-7 w-7 shrink-0 items-center justify-center rounded-lg bg-violet-500/10 text-violet-400 text-xs font-bold">4</span>
          <span class="text-sm font-medium text-slate-200">fill: "forwards" holds final state after animation</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-7 w-7 shrink-0 items-center justify-center rounded-lg bg-violet-500/10 text-violet-400 text-xs font-bold">5</span>
          <span class="text-sm font-medium text-slate-200">Observer disconnects — runs exactly once per load</span>
        </li>
      </ul>
    </div>

  </div>

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

  <script>
    const { animate, inView, stagger } = Motion;

    if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {

      // Single element reveals
      inView('[data-reveal]', function(info) {
        animate(info.target, { opacity: [0, 1], y: [32, 0] }, {
          duration: 0.7,
          easing: [0.22, 1, 0.36, 1],
        });
      }, { margin: '0px 0px -15% 0px' });

      // Stagger grid (fade up)
      inView('[data-stagger]', function(info) {
        animate(info.target.children, { opacity: [0, 1], y: [24, 0] }, {
          duration: 0.5,
          easing: [0.22, 1, 0.36, 1],
          delay: stagger(0.08),
        });
      }, { margin: '0px 0px -10% 0px' });

      // Stagger list (fade from left)
      inView('[data-stagger-x]', function(info) {
        animate(info.target.children, { opacity: [0, 1], x: [-24, 0] }, {
          duration: 0.45,
          easing: [0.22, 1, 0.36, 1],
          delay: stagger(0.07),
        });
      }, { margin: '0px 0px -10% 0px' });
    }
  </script>
</body>
</html>

The from-state (opacity: 0, translate: 0 2rem) lives in CSS, not in JS. This means elements start invisible before the script runs — no flash of unstyled content and no layout shift.

inView() observes each element and fires once when it enters the viewport. The margin: '0px 0px -15% 0px' root margin delays triggering until the element is 15% into the viewport rather than at the very edge, giving it space to animate in fully.

For grids, pass target.children to animate() along with delay: stagger(0.08). Motion generates a per-element delay of index × 80ms. Unlike GSAP’s stagger which requires a parent trigger, here the inView is on the grid container and animate walks the children directly.

fill: "forwards" is not needed — Motion sets the final keyframe as the element’s inline style after the animation completes, which is the WAAPI equivalent of GSAP’s gsap.set().