Description
Texts Reveal staggers two lines of copy into view with a combined translate, opacity, and blur animation. The headline lands first; the supporting line follows a beat later, letting the eye settle on the primary message before the secondary context arrives. The exit is intentionally decoupled: every line fades out at the same speed with no Y return, so dismissing the block reads as a single quiet fade rather than a reverse of the entrance.
Example
Welcome aboardLet us set up your workspace.
CSS
:root {
--stagger-dur: 600ms;
--stagger-distance: 12px;
--stagger-stagger: 40ms;
--stagger-blur: 3px;
--stagger-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
/* Lines start translated down + blurred + invisible; .is-shown
on the parent flips them to their resting state. The second
line's transition-delay holds it back by --stagger-stagger
so the eye lands on the headline first. */
.t-stagger-line {
display: block;
opacity: 0;
transform: translateY(var(--stagger-distance));
filter: blur(var(--stagger-blur));
transition:
opacity var(--stagger-dur) var(--stagger-ease),
transform var(--stagger-dur) var(--stagger-ease),
filter var(--stagger-dur) var(--stagger-ease);
will-change: transform, opacity, filter;
}
.t-stagger-line--2 {
transition-delay: var(--stagger-stagger);
}
.t-stagger.is-shown .t-stagger-line {
opacity: 1;
transform: translateY(0);
filter: blur(0);
}
/* Exit decouples from the stagger: same fade for every line,
no Y return, no blur — so the disappearance reads as a
single quiet fade instead of a reverse reveal. */
.t-stagger.is-hiding .t-stagger-line {
opacity: 0;
transform: translateY(0);
filter: blur(0);
transition:
opacity 200ms ease,
transform 0s linear,
filter 0s linear;
transition-delay: 0s;
}
@media (prefers-reduced-motion: reduce) {
.t-stagger-line {
transition: none !important;
}
}
React
import { useEffect, useState } from "react";
import "./texts-reveal.css"; // paste the CSS above
function Reveal() {
const [shown, setShown] = useState(false);
useEffect(() => {
const id = requestAnimationFrame(() => setShown(true));
return () => cancelAnimationFrame(id);
}, []);
return (
<div className={"t-stagger" + (shown ? " is-shown" : "")}>
<strong className="t-stagger-line t-stagger-line--1">
Welcome aboard
</strong>
<span className="t-stagger-line t-stagger-line--2">
Let us set up your workspace.
</span>
</div>
);
}
export function TextsReveal() {
const [key, setKey] = useState(0);
return (
<div>
<Reveal key={key} />
<button onClick={() => setKey((k) => k + 1)}>Replay</button>
</div>
);
}
Variables
| Variable | Default | Notes |
|---|---|---|
--stagger-dur | 600ms | sourced from --p18-dur |
--stagger-distance | 12px | sourced from --p18-distance |
--stagger-stagger | 40ms | sourced from --p18-stagger |
--stagger-blur | 3px | sourced from --p18-blur |
--stagger-ease | cubic-bezier(0.22, 1, 0.36, 1) | sourced from --p18-ease |
Credit
Adapted from Texts Reveal on transitions.dev by Jakub Antalik. Original source: github.com/Jakubantalik/transitions.dev.