Text Reveal
The headline comes in letter by letter, and screen readers get the whole sentence.
Preview
"use client";
import { useDemo } from "../demo-context";
import { TextReveal } from "./TextReveal";
export default function Demo() {
const { play } = useDemo();
return (
<p style={{ margin: 0, maxWidth: 340, fontFamily: "var(--fluid-font)", fontSize: "clamp(26px, 9cqi, 40px)", lineHeight: 1.05, fontWeight: 600, letterSpacing: "-0.03em", color: "var(--fluid-text)", textAlign: "center" }}>
<TextReveal text="Every letter arrives in its own time." play={play} />
</p>
);
}Installation
npx @usefluid/cli add text-revealpnpm dlx @usefluid/cli add text-revealyarn dlx @usefluid/cli add text-revealbunx @usefluid/cli add text-revealInstall the engine.
Terminal npm i @usefluid/motionCopy these files into your project.
components/fluid/text-reveal/TextReveal.tsx "use client"; import { springCss, springs } from "@usefluid/motion"; import { useEffect, useState } from "react"; import styles from "./TextReveal.module.css"; const spring = springCss(springs.bouncy); export function TextReveal({ text, play = true, stagger = 28 }: { text: string; play?: boolean; stagger?: number }) { const [shown, setShown] = useState(false); useEffect(() => { setShown(false); if (!play) return; let second = 0; const first = requestAnimationFrame(() => { second = requestAnimationFrame(() => setShown(true)); }); return () => { cancelAnimationFrame(first); cancelAnimationFrame(second); }; }, [play, text]); let index = 0; return ( <span className={styles.root} data-play={shown} style={{ "--easing": spring.easing, "--duration": `${spring.duration}ms` } as React.CSSProperties} > <span className={styles.srOnly}>{text}</span> <span aria-hidden> {text.split(" ").map((word, w) => ( <span key={w} className={styles.word}> {word.split("").map((char, c) => ( <span key={c} className={styles.char} style={{ transitionDelay: shown ? `${index++ * stagger}ms` : "0ms" }}> {char} </span> ))} {" "} </span> ))} </span> </span> ); }components/fluid/text-reveal/TextReveal.module.css .root { display: inline-block; } .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); } .word { display: inline-block; white-space: pre; } .char { display: inline-block; opacity: 0; transform: translateY(0.5em) scale(0.8); transition-property: opacity, transform; transition-duration: 180ms, var(--duration); transition-timing-function: ease, cubic-bezier(0.34, 1.56, 0.64, 1); } @supports (transition-timing-function: linear(0, 1)) { .char { transition-timing-function: ease, var(--easing); } } .root[data-play="true"] .char { opacity: 1; transform: none; } @media (prefers-reduced-motion: reduce) { .char { opacity: 1; transform: none; transition: none; } }Import the theme once, then point its variables at your own tokens.
layout.tsx import "@usefluid/motion/theme.css";
Usage
import { TextReveal } from "./TextReveal";
<h1><TextReveal text="Hello, Ana." /></h1>API reference
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | — | |
play | boolean | true | |
stagger | number | 28 | Milliseconds between letters. |
Keyboard and screen readers
The whole text is read at once.
Every colour comes from the --fluid-* variables, and the piece is yours to change.
How it is made
The spring is turned into a CSS linear() curve, so each letter animates with CSS alone, no JavaScript per frame. Each letter waits a little longer than the one before. Words never break in the middle.
For your agent
For your agent
Add Text Reveal to my project with the Fluid library. Use TextReveal on a section headline, once per page. Read my project first. Use my logo, my colours and my font through the --fluid-* variables. Keep 44px touch targets, visible focus and reduced motion. If I have no SVG logo, ask before making one up.