beta

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>)
Statusbeta
Keywordssplit-text, blur-fade, stagger, reveal, headline, hero, text-animate, mask

When to use

Avoid when

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/motion.js, core/observe.js, components/text-reveal/text-reveal.js, components/text-reveal/text-reveal.css.

Usage

Canonical markup — start from it 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(13rem,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().
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).
textstringText to display (otherwise the child content). Changing it replays the animation.

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.