Text reveal
Free · MITReveals a heading word by word or letter by letter with a staggered blur, fade and rise when it scrolls into view or on demand.
Installation
$ npx shadcn@latest add https://goodui.dev/r/text-reveal.jsonThe file lands in components/goodui/text-reveal.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/text-reveal works too.
Usage
import { TextReveal } from "@/components/goodui/text-reveal"
export function FeaturesIntro() {
return (
<section className="py-24 text-center">
<TextReveal
as="h2"
text="Every booking, invoice and reminder in one calm place"
className="mx-auto max-w-2xl text-4xl font-semibold tracking-tight text-balance"
/>
</section>
)
}When to use
- A section heading or tagline that should reveal word by word when it scrolls into view.
- Step-by-step flows where text must hide and show again on demand (play={true/false}), such as onboarding slides.
- Sequencing: start the next animation when the text finishes, with onComplete.
- A normal heading that just needs an entrance: heading with animation="words", "letters" or "blur-in" is simpler and also gives size, font and treatment. Use TextReveal only when you need play control, replays, onComplete or custom timing.
- Paragraphs and body copy: staggering long text slows reading. Use text with animation="fade-up" or no animation.
- Rotating one word inside a sentence: use word-rotate.
| If you need | Use |
|---|---|
| Headline that reveals once on scroll | defaults (by="word", trigger="view", once) |
| Short, punchy word (logo, number) | by="char" |
| Replay every time it enters the viewport | once={false} |
| Above the fold, right away | trigger="mount" |
| Driven by your own state (slides, tabs) | play={isActive} |
| Entrance on a Core heading with no extra control | heading with animation="words" |
Examples
Controlled by a slide index
<TextReveal
as="h3"
text={slides[index].title}
key={index}
play={active}
onComplete={() => setShowCta(true)}
/>Letter by letter, faster
<TextReveal as="h1" by="char" text="Brightside" trigger="mount" stagger={0.03} blur={4} className="text-6xl font-black" />API reference
| Prop | Type | Default |
|---|---|---|
| text* The text to reveal; a plain string so it can be split (screen readers get it whole). | string | — |
| by Animate word by word or character by character. | "word" | "char" | "word" |
| as Element to render, e.g. "h1" for a page title. | "h1" | "h2" | "h3" | "h4" | "p" | "span" | "div" | "p" |
| trigger "view" plays when the text scrolls into view, "mount" plays right away. Ignored when `play` is set. | "view" | "mount" | "view" |
| play Controlled mode: true reveals, false hides. Leave undefined to use `trigger`. | boolean | — |
| once With trigger="view": play only the first time it enters the viewport. | boolean | true |
| delay Seconds before the first word or character. | number | 0 |
| stagger Seconds between two words or characters. Defaults to 0.06 for words and 0.018 for characters. | number | — |
| duration Seconds per unit. | number | 0.5 |
| blur Starting blur in px. | number | 8 |
| onComplete Called when the last word or character has finished revealing. | () => void | — |
Also accepts every <element> attribute.
Accessibility
- The full text is rendered once in an sr-only span; the animated pieces are aria-hidden, so screen readers read it as one sentence.
- Use as="h1"…"h4" so the heading keeps its place in the document outline.
- Words are kept whole (each word is a no-wrap inline-block), so lines never break inside a word.
Motion
- Each word (or character) rises 0.35em, fades in and unblurs from `blur` px (8) over `duration` s (0.5) on ease [0.22, 1, 0.36, 1].
- Units are staggered by `stagger` s: 0.06 for words, 0.018 for characters by default, after `delay`.
- trigger="view" plays when 35% of the element is visible; play={false} hides it again in 0.2 s.
- Reduced motion: no rise, blur or stagger; the whole text fades in over 0.2 s.
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- text must be a plain string; markup and links inside it are not supported. Style the whole element with className.
- Set as for semantics (default is <p>); styling does not come from as.
- play overrides trigger. Leave play undefined to use trigger.
- Durations, delay and stagger are seconds, not milliseconds.
- To replay a new string, change text or key; with once (the default), a scroll-triggered reveal only plays the first time.
- by="char" splits into grapheme clusters (emoji and combining marks stay whole). Arabic-script text animates by word instead, because splitting joined letters breaks their shapes; RTL text otherwise works as is.
FAQ
Should I use this or the Core heading's animation?
For a heading that animates once on view, the Core heading with animation="words" or "letters" is simpler. TextReveal adds controlled play, replays, onComplete and custom stagger, duration and blur.
Is the text in the HTML before it animates?
Yes. The full string is rendered in an sr-only span and as split words; only their opacity, position and blur animate.
Related
Layered heading: semantic level and size decoupled, sans/serif/mono/display font slots you map to your own fonts, eight ink treatments (gradient, muted tail, outline, marker highlight, hand-drawn squiggle, shimmer…), serif-italic emphasis via _words_, and on-view word, letter and mask reveals.
Every body-copy role in one primitive: paragraph, lead, small, caption, overline, eyebrow pill, quote with attribution and inline code, with tones, readable measures, truncate or line clamp, and on-view entrances.
Swaps words in place with a vertical slide and blur while the box springs to each word's width, so the sentence glides instead of jumping.
Types and deletes a list of phrases with a human rhythm and a caret that only blinks while idle; works as an animated input placeholder.