Animate text into view as the user scrolls, line by line, word by word, or character by character. CSS and JavaScript approaches with copyable code.
Text lines slide up into view from below as they enter the viewport. The outer element has overflow:hidden, which clips text below the baseline. The inner text starts at translateY(110%) and animates to translateY(0) using animation-timeline: view().
Uses a named view-timeline on the section + overflow:hidden on the wrapper to create a clean line-reveal as the section scrolls into view.
Each word animates individually with a staggered animation-delay, creating a ripple effect across the line. Words use animation-timeline: view() with inline animation-delay applied via HTML attributes.
Split your text into <span> elements. Apply increasing animation-delay to each word span for the stagger effect.
A cross-browser approach: elements start hidden with opacity: 0; translateY(30px). The Intersection Observer API detects when each element enters the viewport and adds a .visible class that triggers the CSS transition. Works in all modern browsers (97%+ global support), no CSS scroll-timeline needed.
The observer fires when 30% of the element is visible. Toggle the speed to see slow or fast reveals.
In a fullscreen layout, text reveals work best when tied to section transitions, not scroll position. fullPage.js fires the afterLoad callback the moment a section snaps into view, so you just toggle a CSS class. No IntersectionObserver, no animation-timeline browser issues. Works everywhere. Looks amazing.
fullPage.js adds
The line reveal is a pure CSS text reveal animation that uses two layers: an outer <span> with overflow: hidden that acts as a mask, and an inner <span> that starts positioned below the visible area. Because the parent clips overflow, the inner text is completely invisible until the animation begins:
HTML
<span style="overflow: hidden; display: block">
<span class="reveal-line">This line slides into view</span>
</span>
CSS
.reveal-line {
display: block;
transform: translateY(110%);
animation: slide-up both linear;
animation-timeline: view();
animation-range: entry 45% entry 75%;
}
@keyframes slide-up {
to { transform: translateY(0); }
}
The animation is driven by animation-timeline: view(), a CSS Scroll Timeline property that links animation progress directly to scroll position instead of clock time. The animation-range: entry 45% entry 75% defines the scroll window: the reveal starts when 45% of the section has entered the viewport and completes at 75%.
The effect dropdown in the demo above swaps the @keyframes rule. "Slide Up" uses translateY(110%) → translateY(0); "Slide Down" reverses the direction; "Blur" adds filter: blur(6px) → blur(0) for a focus-pull effect; "Scale" grows from scale(0.4); and "Flip" rotates from rotateX(90deg) using the parent's perspective: 600px. All of these are pure CSS; no JavaScript involved.
Browser support: animation-timeline works in Chrome 115+, Edge 115+, and Safari 26+. Firefox does not support it yet. For cross-browser production use, wrap these styles in @supports (animation-timeline: view()) and provide a JavaScript fallback.
The word reveal splits text into individual <span> elements, each wrapped in an overflow: hidden container. Every word gets the same base animation, but with an incrementally increasing animation-delay to create a staggered cascade:
HTML
<!-- Each word gets a stagger index via --i -->
<span style="overflow:hidden">
<span class="word" style="--i:0">Every</span>
</span>
<span style="overflow:hidden">
<span class="word" style="--i:1">word</span>
</span>
<span style="overflow:hidden">
<span class="word" style="--i:2">reveals</span>
</span>
CSS
.word {
display: inline-block;
transform: translateY(100%);
animation: slide-up both linear;
animation-timeline: view();
animation-range: entry 40% entry 80%;
animation-delay: calc(var(--i) * 5%);
}
The stagger of 5% per word with 6 words means the last word starts 30% after the first, tight enough to feel cohesive, wide enough to read as a sequence. Combined with animation-timeline: view(), the delay offsets each word's reveal within the scroll range.
The effect options work identically to the line reveal: "Slide Up + Blur" adds a defocus-to-focus transition; "Scale" grows each word from a small size; "Flip" rotates each word into place around the X axis. The stagger control in the demo above adjusts the delay multiplier in real time so you can find the right timing by scrolling through the preview.
This is one of the most searched-for text reveal animation CSS patterns. The pure CSS approach here means zero runtime JavaScript; the browser's compositor thread handles the entire animation, keeping the main thread free.
The Intersection Observer approach is the cross-browser alternative to CSS scroll-timeline. Each target element starts hidden and transitions in when a .visible class is added:
CSS
.reveal {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
JavaScript
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.3 });
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
The key difference from the pure CSS approach is that Intersection Observer fires a one-shot trigger: it adds the class and calls unobserve(). The element animates once and stays visible. There is no native "scrubbing" (reversing on scroll back) unless you add logic to remove the class when the element leaves the viewport.
Configuration is minimal: threshold controls how much of the element must be visible before triggering (0.3 = 30%), and rootMargin can offset the detection zone (e.g., '-50px' to trigger slightly after the element enters). The transition speed is set entirely in CSS via the transition-duration property on the target elements; the JavaScript only toggles the class.
This works in every modern browser including Firefox, Safari, Chrome, and Edge. For stagger effects, apply increasing transition-delay values to sibling elements via CSS or inline styles, the same technique as the pure CSS version, but triggered by the observer class toggle instead of scroll-timeline.
Both approaches produce similar visual results. The key differences are browser support, performance characteristics, and flexibility.
| CSS scroll-timeline | JavaScript (IntersectionObserver) | |
|---|---|---|
| Browser support | Chrome, Edge, Safari 26+ only | All modern browsers |
| Performance | Compositor thread (no JS cost) | Main thread (minimal JS cost) |
| Scrubbing (reverse on scroll back) | Automatic | Requires extra logic |
| Stagger delays | animation-delay per span | CSS animation-delay per span |
| Trigger control | animation-range | threshold + rootMargin |
| Complexity | CSS only, no JS | ~15 lines of JS |
For production sites, use CSS with an @supports check and an IntersectionObserver fallback for Firefox users.
overflow:hidden container. Inside, add an element starting at translateY(110%) that animates to translateY(0) using animation-timeline: view(). The overflow clip hides the text below the baseline until it rises into view.<span> elements. Apply increasing animation-delay inline styles (0ms, 60ms, 120ms…). Each span gets the same animation-timeline: view() linked to the parent section's position, but starts at a different time offset.animation-timeline: view() approach does not work in Firefox by default. For full cross-browser support, use the Intersection Observer approach (Demo 3 above). You can also use @supports to serve CSS scroll-timeline to supported browsers while falling back to IntersectionObserver for Firefox.animation-delay values as inline styles: style="animation-delay:0ms", style="animation-delay:60ms", etc. For dynamically generated content, use a script to set el.style.animationDelay = (index * 60) + 'ms' on each word or character span after splitting the text.translateY(110%) it sits below the clip boundary and is invisible. As it animates toward translateY(0) it enters the visible area from below, creating the reveal effect. Without overflow:hidden, you would see the text below its final position.Add scroll transitions, parallax and animations to your site with fullPage.js. One component, 80+ effects.
Powering fullscreen design for Google, Sony, BBC, eBay & more