Text reveal

Free · MIT

Reveals a heading word by word or letter by letter with a staggered blur, fade and rise when it scrolls into view or on demand.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/text-reveal.json

The 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

Use it for
  • 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.
Reach for something else
  • 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 needUse
Headline that reveals once on scrolldefaults (by="word", trigger="view", once)
Short, punchy word (logo, number)by="char"
Replay every time it enters the viewportonce={false}
Above the fold, right awaytrigger="mount"
Driven by your own state (slides, tabs)play={isActive}
Entrance on a Core heading with no extra controlheading 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

PropTypeDefault
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.
booleantrue
delay
Seconds before the first word or character.
number0
stagger
Seconds between two words or characters. Defaults to 0.06 for words and 0.018 for characters.
number—
duration
Seconds per unit.
number0.5
blur
Starting blur in px.
number8
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