Clip Reveal
Clip Reveal
Text lines and panels reveal from below the baseline using clip-path animations with Motion One.
Dependencies
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clip Reveal — 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>
.clip-line {
display: block;
clip-path: inset(0 0 100% 0);
transform: translateY(12px);
}
.hero-sub {
clip-path: inset(0 0 100% 0);
}
.reveal-body {
clip-path: inset(0 0 100% 0);
}
@media (prefers-reduced-motion: reduce) {
.clip-line, .hero-sub, .reveal-body, .block-cover {
clip-path: none !important;
transform: none !important;
opacity: 1 !important;
}
}
</style>
</head>
<body class="bg-stone-50 text-stone-900 p-8 font-sans">
<!-- Hero text reveal -->
<div class="mx-auto mb-32 max-w-3xl pt-16">
<span class="mb-6 inline-block text-[0.7rem] font-bold uppercase tracking-[0.18em] text-stone-400">Clip Reveal</span>
<h1 class="overflow-hidden text-[clamp(2.5rem,6vw,4rem)] font-black leading-tight text-stone-900">
<span class="clip-line">Words appear</span>
<span class="clip-line">from below the</span>
<span class="clip-line">baseline.</span>
</h1>
<p class="hero-sub mt-6 max-w-md text-base leading-relaxed text-stone-500">Scroll down to see panels, images, and text blocks reveal with clip-path animations using Motion One.</p>
</div>
<div class="mx-auto mb-20 max-w-3xl h-px bg-stone-200"></div>
<!-- Block reveal + text -->
<div id="block-section" class="mx-auto mb-24 max-w-3xl grid gap-6 md:grid-cols-2 items-start">
<div class="relative overflow-hidden rounded-xl bg-stone-200 aspect-[4/3]">
<div class="absolute inset-0 flex items-center justify-center text-xs font-semibold uppercase tracking-widest text-stone-400">Image placeholder</div>
<div id="block-cover-1" class="block-cover absolute inset-0 bg-stone-900 origin-left"></div>
</div>
<div class="flex flex-col gap-4 pt-2">
<span class="text-[0.7rem] font-bold uppercase tracking-[0.14em] text-stone-400">Editorial reveal</span>
<h3 class="overflow-hidden text-2xl font-black leading-tight text-stone-900">
<span class="clip-line">The cover slides</span>
<span class="clip-line">away to the left.</span>
</h3>
<p class="reveal-body text-sm leading-relaxed text-stone-500">A solid-color cover panel scales to zero horizontally while the content underneath is already in place. The visual effect is a wipe — a classic editorial reveal used by magazines and agency sites.</p>
</div>
</div>
<div class="mx-auto mb-20 max-w-3xl h-px bg-stone-200"></div>
<!-- Stats reveal -->
<div id="stats-section" class="mx-auto mb-24 max-w-3xl">
<h2 class="mb-12 overflow-hidden text-2xl font-black leading-tight text-stone-900">
<span class="clip-line">Numbers hit harder</span>
<span class="clip-line">when they arrive.</span>
</h2>
<div class="grid grid-cols-3 gap-6">
<div class="border-t-2 border-stone-200 pt-4">
<div class="overflow-hidden text-[2.5rem] font-black text-stone-900"><span class="clip-line">98%</span></div>
<p class="mt-1 text-xs text-stone-400">Client satisfaction</p>
</div>
<div class="border-t-2 border-stone-200 pt-4">
<div class="overflow-hidden text-[2.5rem] font-black text-stone-900"><span class="clip-line">3.2s</span></div>
<p class="mt-1 text-xs text-stone-400">Avg page load time</p>
</div>
<div class="border-t-2 border-stone-200 pt-4">
<div class="overflow-hidden text-[2.5rem] font-black text-stone-900"><span class="clip-line">40+</span></div>
<p class="mt-1 text-xs text-stone-400">Projects shipped</p>
</div>
</div>
</div>
<div class="h-16"></div>
<script>
const { animate, inView } = Motion;
// Hero text lines — on load
const heroLines = document.querySelectorAll('.hero .clip-line, .mx-auto.pt-16 .clip-line');
animate(
document.querySelectorAll('.clip-line'),
{ clipPath: ['inset(0 0 100% 0)', 'inset(0 0 0% 0)'], y: [12, 0] },
{ duration: 0.7, ease: [0.16, 1, 0.3, 1], delay: (_, i) => i * 0.12 }
);
animate(
document.querySelector('.hero-sub'),
{ clipPath: ['inset(0 0 100% 0)', 'inset(0 0 0% 0)'] },
{ duration: 0.6, ease: [0.16, 1, 0.3, 1], delay: 0.5 }
);
// Block reveal — inView
inView('#block-section', () => {
animate(
'#block-cover-1',
{ scaleX: [1, 0] },
{ duration: 0.8, ease: [0.76, 0, 0.24, 1], delay: 0.1 }
);
const lines = document.querySelectorAll('#block-section .reveal-title .clip-line');
animate(
lines,
{ clipPath: ['inset(0 0 100% 0)', 'inset(0 0 0% 0)'], y: [10, 0] },
{ duration: 0.6, ease: [0.16, 1, 0.3, 1], delay: (_, i) => 0.3 + i * 0.1 }
);
animate(
'#block-section .reveal-body',
{ clipPath: ['inset(0 0 100% 0)', 'inset(0 0 0% 0)'] },
{ duration: 0.5, ease: [0.16, 1, 0.3, 1], delay: 0.55 }
);
}, { amount: 0.3 });
// Stats reveal — inView
inView('#stats-section', () => {
const lines = document.querySelectorAll('#stats-section .clip-line');
animate(
lines,
{ clipPath: ['inset(0 0 100% 0)', 'inset(0 0 0% 0)'], y: [8, 0] },
{ duration: 0.6, ease: [0.16, 1, 0.3, 1], delay: (_, i) => i * 0.1 }
);
}, { amount: 0.4 });
</script>
</body>
</html>
clip-path: inset(0 0 100% 0) hides the element by clipping everything from the bottom. Animating the bottom inset to 0% reveals it upward. Combining with a small y offset creates the feeling of the text rising through a slot.
The hero lines play on page load. Sections below use inView() from Motion One, which accepts an amount option (0–1) for how much of the element must be visible before firing. 0.3 works well for content blocks.
The block-cover wipe uses scaleX: [1, 0] with transform-origin: left center on an overlay element. The content beneath sits in place — there’s no actual reveal happening on the content, only the cover shrinking away.