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.

CategoryAnimated text
TypeWeb Component (<mv-typewriter>)
Statusbeta
Keywordstypewriter, 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/marvelous

AI 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

NameTypeDefaultDescription
wordsstringPhrases separated by commas (or by | if they contain commas). Otherwise: <span> children (their classes stay on the phrase) or plain text.
loopbooleanDeletes and cycles through the phrases in a loop. Without loop, stops on the last phrase. Hover or focus holds the current phrase.
caretbar | block | underscore | nonebarCaret style: solid while typing, a few blinks at rest, then steady.
speednumber (ms)55Average delay between two letters.
delete-speednumber (ms)28Delay between two deletions.
pausenumber (ms)1800Reading time before deleting.
delaynumber (ms)0Delay before the first letter.
jitternumber 0-10.45Typing irregularity (0 = metronome).
colorslist of CSS colorsColor per phrase, cycling (tokens accepted).
triggerview | load | hover | manualviewTrigger (combinable). view starts on first view, hover replays a finished sequence, manual shows the final text and waits for replay().

Methods

NameDescription
replay()Restarts from the first phrase.

Events

NameDescription
mv-changeNew phrase started (detail.index).
mv-endLast phrase typed (without loop).

CSS classes

NameDescription
mv-typewriter-phraseOne per phrase (generated, aria-hidden). The one on screen holds -typed, -caret and -pending; the others are hidden="until-found".
mv-typewriter-typed / -pendingTyped part and untyped rest of the current phrase; the rest keeps its place but is not painted.
mv-typewriter-caretZero-width caret anchor; the caret is drawn by its ::before.

CSS variables

NameDefaultDescription
--mv-typewriter-caretvar(--mv-accent)Caret color.
--mv-typewriter-blink1.1sBlink 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.