Fade Up
Fade Up
Elements fade and rise into view as they enter the viewport using GSAP ScrollTrigger.
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 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) {
.fade-up { 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 fade-up">
<span class="mb-6 inline-block rounded-full border border-indigo-500/20 bg-indigo-500/10 px-3.5 py-1 text-[0.7rem] font-semibold uppercase tracking-widest text-indigo-400">Scroll Reveal</span>
<h1 class="mb-4 text-5xl font-black leading-tight text-slate-100">Elements fade up<br>as you scroll</h1>
<p class="mx-auto mb-8 max-w-sm text-lg leading-relaxed text-slate-500">Scroll down to see each section animate into view using GSAP ScrollTrigger.</p>
<a href="#" class="inline-block cursor-default rounded-xl bg-indigo-600 px-7 py-2.5 text-sm font-semibold text-white">Get started</a>
</div>
<!-- Section 1 -->
<div class="mx-auto mb-24 max-w-3xl fade-up">
<p class="mb-3 text-xs font-semibold uppercase tracking-[0.12em] text-slate-500">Why it works</p>
<h2 class="mb-4 text-3xl font-black leading-tight text-slate-100">Subtle motion<br>builds focus</h2>
<p class="max-w-[560px] text-base leading-relaxed text-slate-400">Fade-up reveals guide the eye naturally through the page. Each element earns attention in sequence, reducing cognitive load and increasing readability.</p>
</div>
<div class="mx-auto mb-24 max-w-3xl h-px bg-white/[0.06]"></div>
<!-- Cards -->
<div class="mx-auto mb-24 max-w-3xl grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="fade-up rounded-2xl border border-white/[0.08] bg-white/[0.04] p-6">
<div class="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-500/15 text-indigo-400">
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"/></svg>
</div>
<p class="mb-1 text-sm font-semibold text-slate-100">Instant feedback</p>
<p class="text-xs leading-relaxed text-slate-500">Animations respond to scroll position in real time, with no jank.</p>
</div>
<div class="fade-up rounded-2xl border border-white/[0.08] bg-white/[0.04] p-6">
<div class="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-500/15 text-indigo-400">
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<p class="mb-1 text-sm font-semibold text-slate-100">Accessible</p>
<p class="text-xs leading-relaxed text-slate-500">Respects prefers-reduced-motion — elements show immediately for users who prefer it.</p>
</div>
<div class="fade-up rounded-2xl border border-white/[0.08] bg-white/[0.04] p-6">
<div class="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-500/15 text-indigo-400">
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z"/></svg>
</div>
<p class="mb-1 text-sm font-semibold text-slate-100">5 lines of JS</p>
<p class="text-xs leading-relaxed text-slate-500">GSAP handles all the heavy lifting. You just describe the from-state.</p>
</div>
<div class="fade-up rounded-2xl border border-white/[0.08] bg-white/[0.04] p-6">
<div class="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-500/15 text-indigo-400">
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5"/></svg>
</div>
<p class="mb-1 text-sm font-semibold text-slate-100">Framework-free</p>
<p class="text-xs leading-relaxed text-slate-500">Works with vanilla JS, React, Vue, Astro — any stack that renders HTML.</p>
</div>
</div>
<div class="mx-auto mb-24 max-w-3xl h-px bg-white/[0.06]"></div>
<!-- Section 2 -->
<div class="mx-auto mb-24 max-w-3xl fade-up">
<p class="mb-3 text-xs font-semibold uppercase tracking-[0.12em] text-slate-500">Customization</p>
<h2 class="mb-4 text-3xl font-black leading-tight text-slate-100">Tune every<br>parameter</h2>
<p class="max-w-[560px] text-base leading-relaxed text-slate-400">Control duration, distance, easing, and the scroll trigger point. GSAP's ScrollTrigger offers precise control over when animations start and end relative to the viewport.</p>
</div>
<div class="mx-auto mb-24 max-w-3xl grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="fade-up rounded-2xl border border-white/[0.08] bg-white/[0.04] p-6">
<p class="mb-1 text-sm font-semibold text-slate-100">duration</p>
<p class="text-xs leading-relaxed text-slate-500">Default 0.7s. Shorter for micro-elements, longer for hero sections.</p>
</div>
<div class="fade-up rounded-2xl border border-white/[0.08] bg-white/[0.04] p-6">
<p class="mb-1 text-sm font-semibold text-slate-100">y offset</p>
<p class="text-xs leading-relaxed text-slate-500">Default 40px. Larger values create more dramatic entrances.</p>
</div>
<div class="fade-up rounded-2xl border border-white/[0.08] bg-white/[0.04] p-6">
<p class="mb-1 text-sm font-semibold text-slate-100">ease</p>
<p class="text-xs leading-relaxed text-slate-500">power2.out gives a natural deceleration. Try expo.out for luxury feel.</p>
</div>
<div class="fade-up rounded-2xl border border-white/[0.08] bg-white/[0.04] p-6">
<p class="mb-1 text-sm font-semibold text-slate-100">start trigger</p>
<p class="text-xs leading-relaxed text-slate-500">"top 85%" means the top of the element hits 85% down the viewport.</p>
</div>
</div>
<div class="h-16"></div>
<script>
gsap.registerPlugin(ScrollTrigger);
gsap.utils.toArray('.fade-up').forEach((el) => {
gsap.from(el, {
scrollTrigger: {
trigger: el,
start: 'top 88%',
toggleActions: 'play none none none',
},
opacity: 0,
y: 40,
duration: 0.7,
ease: 'power2.out',
});
});
</script>
</body>
</html>
Each element with .fade-up gets its own independent ScrollTrigger that fires once when the element’s top edge hits 88% down the viewport. No JavaScript class toggling — GSAP animates directly from the defined from state to the element’s natural CSS position.
The toggleActions: 'play none none none' setting means the animation only plays once on enter and never reverses on scroll-up, which is the expected behavior for page load reveals.
Pair with a stagger for grids: wrap elements in a container and use gsap.from('.child', { stagger: 0.08, scrollTrigger: { trigger: '.parent' } }) instead of individual triggers.