Text Reveal <mv-text-reveal>

Text reveal split into letters, words or lines with 6 staggered presets (fade, blur, slide, 3D rotate, zoom, mask), triggered when it enters the viewport; inline markup is preserved.

CategoryAnimated text
TypeWeb Component (<mv-text-reveal>)
Statusstable
Keywordssplit-text, blur-fade, stagger, reveal, headline, hero, text-animate, mask

When to use

  • A hero headline should animate in by letters, words or lines when it enters the viewport
  • An entrance preset (fade, blur, slide, 3D rotate, zoom, mask) must keep inline markup like em or strong
  • A heading should replay its reveal on hover or from script

Avoid when

  • The reveal should be driven by scroll progress rather than a one-shot entrance → use Scroll Text instead
  • Whole cards or sections should fade in, not split text → use Scroll Reveal instead
  • Long body copy; splitting paragraphs into staggered pieces delays reading

Install

node scripts/add.mjs text-reveal --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["text-reveal"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/i18n.js, core/motion.js, core/observe.js, components/text-reveal/text-reveal.js, components/text-reveal/text-reveal.css.

Usage

Canonical markup, to start from and customize with attributes, data-* and CSS variables:

<div id="tr-demo" style="display:grid;gap:2rem;width:min(100%,46rem);margin-inline:auto">
  <div style="text-align:center">
    <p style="margin:0 0 .5rem;font-size:.8rem;letter-spacing:.14em;text-transform:uppercase;color:var(--mv-fg-muted)">
      <mv-text-reveal effect="blur" by="chars" delay="100">Launch · Spring 2026</mv-text-reveal>
    </p>
    <h2 style="margin:0;font:700 clamp(2rem,5vw,3.4rem)/1.05 var(--mv-font-display);letter-spacing:-.035em">
      <mv-text-reveal effect="mask" by="words">Interfaces that make people <em style="font-style:normal;color:var(--mv-accent)">want</em> to click.</mv-text-reveal>
    </h2>
    <p style="margin:.9rem auto 0;max-width:32rem;color:var(--mv-fg-muted);line-height:1.55">
      <mv-text-reveal effect="fade-up" by="lines" delay="450">Native components, polished animations, accessible by default. Copy, paste, ship. In any framework.</mv-text-reveal>
    </p>
  </div>

  <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(13rem,100%),1fr));gap:.75rem">
    <div style="padding:1rem 1.1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface)">
      <small style="color:var(--mv-fg-subtle);font-family:var(--mv-font-mono)">effect="blur"</small>
      <p style="margin:.35rem 0 0;font-size:1.3rem;font-weight:600;letter-spacing:-.02em"><mv-text-reveal effect="blur">Sharp from the very first second.</mv-text-reveal></p>
    </div>
    <div style="padding:1rem 1.1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface)">
      <small style="color:var(--mv-fg-subtle);font-family:var(--mv-font-mono)">effect="rotate" by="chars"</small>
      <p style="margin:.35rem 0 0;font-size:1.3rem;font-weight:600;letter-spacing:-.02em"><mv-text-reveal effect="rotate" by="chars">Every letter counts.</mv-text-reveal></p>
    </div>
    <div style="padding:1rem 1.1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface)">
      <small style="color:var(--mv-fg-subtle);font-family:var(--mv-font-mono)">effect="scale"</small>
      <p style="margin:.35rem 0 0;font-size:1.3rem;font-weight:600;letter-spacing:-.02em"><mv-text-reveal effect="scale" easing="var(--mv-ease-spring)" duration="900">Small detail, big impact.</mv-text-reveal></p>
    </div>
    <div style="padding:1rem 1.1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface)">
      <small style="color:var(--mv-fg-subtle);font-family:var(--mv-font-mono)">effect="slide" by="chars"</small>
      <p style="margin:.35rem 0 0;font-size:1.3rem;font-weight:600;letter-spacing:-.02em"><mv-text-reveal effect="slide" by="chars">Always one step ahead.</mv-text-reveal></p>
    </div>
  </div>

  <div style="display:flex;justify-content:center">
    <button class="mv-button" data-variant="outline" data-size="sm" id="tr-demo-replay">Replay</button>
  </div>
</div>
<script type="module">
  const root = document.getElementById("tr-demo");
  root.querySelector("#tr-demo-replay").addEventListener("click", () => {
    root.querySelectorAll("mv-text-reveal").forEach((el) => el.replay());
  });
</script>

API

Attributes

NameTypeDefaultDescription
effectfade-up | blur | slide | rotate | scale | maskfade-upEntrance preset. mask = pieces rise from behind a clipping line.
bychars | words | lineswordsSplit granularity. lines groups words by visual line (measured at start).
triggerview | load | hover | manualviewTrigger (combinable: "view hover"). manual shows the text and waits for replay(). Can change after mount: the listeners are rewired, a reveal already played stays shown.
durationnumber (ms)700 (900 for mask)Duration per piece.
delaynumber (ms)0Delay before the first piece.
staggernumber (ms)28 / 70 / 160Delay between pieces (letters / words / lines).
easingCSS easingvar(--mv-ease-out)Animation easing (tokens accepted, e.g. var(--mv-ease-spring)).
repeatbooleanReplays every time it enters the viewport (otherwise once). Can change after mount.
textstringText to display (otherwise the child content). Changing it replays the animation; removing it restores the child content.

Methods

NameDescription
replay()Restarts the reveal from the hidden state.

Events

NameDescription
mv-endWhen the last piece has finished entering.

Content structure

NameDescription
(content)Text with optional inline markup (<em>, <strong>, <span>…), preserved after splitting.

CSS variables

NameDescription
--mv-text-reveal-durationDefault duration per piece.
--mv-text-reveal-staggerDefault stagger.
--mv-text-reveal-easingDefault easing.
--mv-text-reveal-delayDefault initial delay.

Accessibility

The real text lives in a visually hidden copy read by screen readers; the split version is aria-hidden. Put the element inside an <h1>/<p> to keep the semantics. Under reduced motion, the text is shown immediately in its final state.