beta
Typewriter <mv-typewriter>
Typewriter for hero lines: types phrases letter by letter with an irregular rhythm and punctuation pauses, erases and cycles them behind a bar, block or underscore caret, inside a box reserved at its final size so nothing around it moves.
| Category | Animated text |
|---|---|
| Type | Web Component (<mv-typewriter>) |
| Status | beta |
| Keywords | typewriter, typing, caret, cursor, rotating, hero, terminal |
When to use
- A hero line should type several phrases in a loop, deleting between them, without pushing the layout around
- A terminal or conversational tone needs realistic irregular typing with pauses on punctuation
- Animated copy must stay readable by screen readers and findable with the browser search
Avoid when
- A single word should change inside a sentence with a smooth width glide → use Word Rotate instead
- Key information only appears after long typing delays; visitors may leave before reading it
Install
node scripts/add.mjs typewriter --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["typewriter"], 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/typewriter/typewriter.js, components/typewriter/typewriter.css.
Usage
Canonical markup, to start from and customize with attributes, data-* and CSS variables:
<div id="tw-demo" style="display:grid;gap:1.75rem;width:min(100%,44rem);margin-inline:auto;text-align:center">
<h2 style="margin:0;font:700 clamp(1.8rem,4.4vw,3rem)/1.1 var(--mv-font-display);letter-spacing:-.035em">
We build<br>
<mv-typewriter loop words="sites that convert., apps people keep., brands that stick." colors="var(--mv-accent), var(--mv-info), var(--mv-success)"></mv-typewriter>
</h2>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(18rem,1fr));gap:.75rem;text-align:start">
<div style="padding:1rem 1.1rem;border-radius:var(--mv-radius-lg);background:var(--mv-gray-950);border:1px solid var(--mv-border);color:var(--mv-gray-100);font:500 .9rem/1.6 var(--mv-font-mono);box-shadow:var(--mv-shadow-md)">
<span style="color:var(--mv-gray-500)">~/project $</span>
<mv-typewriter caret="block" speed="45" delay="600" style="--mv-typewriter-caret:var(--mv-gray-300)">npx mv add typewriter</mv-typewriter>
</div>
<div style="padding:1rem 1.1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface);font-size:1.05rem">
<mv-typewriter caret="underscore" loop pause="1400">
<span>Hi, I’m Iris.</span>
<span>I write your briefs.</span>
<span>I summarize your meetings.</span>
</mv-typewriter>
</div>
</div>
<div style="display:flex;justify-content:center">
<button class="mv-button" data-variant="outline" data-size="sm" id="tw-demo-replay">Replay</button>
</div>
</div>
<script type="module">
const root = document.getElementById("tw-demo");
root.querySelector("#tw-demo-replay").addEventListener("click", () => {
root.querySelectorAll("mv-typewriter").forEach((el) => el.replay());
});
</script>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
words | string | Phrases separated by commas (or by | if they contain commas). Otherwise: <span> children (their classes stay on the phrase) or plain text. | |
loop | boolean | Deletes and cycles through the phrases in a loop. Without loop, stops on the last phrase. Hover or focus holds the current phrase. | |
caret | bar | block | underscore | none | bar | Caret style: solid while typing, a few blinks at rest, then steady. |
speed | number (ms) | 55 | Average delay between two letters. |
delete-speed | number (ms) | 28 | Delay between two deletions. |
pause | number (ms) | 1800 | Reading time before deleting. |
delay | number (ms) | 0 | Delay before the first letter. |
jitter | number 0-1 | 0.45 | Typing irregularity (0 = metronome). |
colors | list of CSS colors | Color per phrase, cycling (tokens accepted). | |
trigger | view | load | hover | manual | view | Trigger (combinable). view starts on first view, hover replays a finished sequence, manual shows the final text and waits for replay(). |
Methods
| Name | Description |
|---|---|
replay() | Restarts from the first phrase. |
Events
| Name | Description |
|---|---|
mv-change | New phrase started (detail.index). |
mv-end | Last phrase typed (without loop). |
CSS classes
| Name | Description |
|---|---|
mv-typewriter-phrase | One per phrase (generated, aria-hidden). The one on screen holds -typed, -caret and -pending; the others are hidden="until-found". |
mv-typewriter-typed / -pending | Typed part and untyped rest of the current phrase; the rest keeps its place but is not painted. |
mv-typewriter-caret | Zero-width caret anchor; the caret is drawn by its ::before. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-typewriter-caret | var(--mv-accent) | Caret color. |
--mv-typewriter-blink | 1.1s | Blink period. |
Accessibility
Every phrase stays real, complete text: typing only hides the untyped rest of the visible phrase, so the box is reserved at its final size and never shifts. Screen readers read all phrases once, in order, from a visually hidden copy; the animated layer is aria-hidden and has no aria-live. Find in page matches the visible phrase and the hidden ones (hidden="until-found"): a match shows that phrase whole and stops the cycle. Hovering or focusing it (or the link around it) holds the current phrase and types back one being erased. The sequence freezes off screen and in a hidden tab. Reduced motion: the text appears at once, with a steady caret. The caret blinks a few times, then stays lit. Forced colors: the caret uses CanvasText. Print shows the whole phrase.