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.
| Category | Animated text |
|---|---|
| Type | Web Component (<mv-text-reveal>) |
| Status | beta |
| Keywords | split-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/marvelousAI 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
| Name | Type | Default | Description |
|---|---|---|---|
effect | fade-up | blur | slide | rotate | scale | mask | fade-up | Entrance preset. mask = pieces rise from behind a clipping line. |
by | chars | words | lines | words | Split granularity. lines groups words by visual line (measured at start). |
trigger | view | load | hover | manual | view | Trigger (combinable: "view hover"). manual shows the text and waits for replay(). |
duration | number (ms) | 700 (900 for mask) | Duration per piece. |
delay | number (ms) | 0 | Delay before the first piece. |
stagger | number (ms) | 28 / 70 / 160 | Delay between pieces (letters / words / lines). |
easing | CSS easing | var(--mv-ease-out) | Animation easing (tokens accepted, e.g. var(--mv-ease-spring)). |
repeat | boolean | Replays every time it enters the viewport (otherwise once). | |
text | string | Text to display (otherwise the child content). Changing it replays the animation. |
Methods
| Name | Description |
|---|---|
replay() | Restarts the reveal from the hidden state. |
Events
| Name | Description |
|---|---|
mv-end | When the last piece has finished entering. |
Content structure
| Name | Description |
|---|---|
(content) | Text with optional inline markup (<em>, <strong>, <span>…), preserved after splitting. |
CSS variables
| Name | Description |
|---|---|
--mv-text-reveal-duration | Default duration per piece. |
--mv-text-reveal-stagger | Default stagger. |
--mv-text-reveal-easing | Default easing. |
--mv-text-reveal-delay | Default 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.