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.
Dependencies
<!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().