Skip to content

COMPONENT

Typewriter

Short text that types itself in the first time somebody sees it. It animates no layout — the familiar CSS version reflows on every frame and this one masks instead — and it draws nothing at all without a script.

Scroll to it, or press replay

Flatty

HTML
<h2 class="typewriter" x-ds-reveal>Flatty</h2>

IT ANIMATES NO LAYOUT, AND EVERY OTHER ONE DOES

The typewriter everybody has written animates width from zero with overflow: hidden. That is a reflow on every frame of every instance — and moving nothing is precisely why a reveal is safe to put above the fold in the first place. This one moves a mask, which is composited, so it costs the same as the lamp: nothing measurable in Cumulative Layout Shift.

Why it is not counted in ch

Because 1ch is not a character. It is the advance of the zero glyph, which is the text's own width only in a monospaced face and only approximately. Filmed, the ch version of this finished a thirteen-character heading reading SELECTED WOR — a character and a half short. It reveals a fraction of its own box instead, which is exact in any face. This library names two families and loads neither, so a consumer whose display face is proportional is the ordinary case, not the exotic one.

Which is why the specimen below is set in the body face rather than the display one. It is not decoration: it is the fixture that makes the failure reachable. Counted in ch, this line stops short — and every other specimen on this page would go on passing, because they are all monospaced and ch happens to be right there.

Scroll to it, or press replay

Proportional, and still exact

HTML
<p class="notice-body typewriter" x-ds-reveal>Proportional, and still exact</p>

That is also why the element takes fit-content: a fraction of a block-level heading is a fraction of the column rather than of the words, and the mask would finish long before the text did.

A list of them

Each types at the same speed per character, so a long title takes longer than a short one — which is what makes several of them read as one machine rather than as three animations that happen to have the same duration.

Scroll to it, or press replay

Flatty A pixel editor that extrudes 2D artwork.
Northwind Fifteen years of orders, migrated in a weekend.
The Bird A tape machine that only ever played backwards.
HTML
<div class="rule-stack" style="width:100%">
  <div class="rule-row">
    <span class="rule-row-key typewriter" x-ds-reveal>Flatty</span>
    <span class="rule-row-value">A pixel editor that extrudes 2D artwork.</span>
  </div>
  <div class="rule-row">
    <span class="rule-row-key typewriter" x-ds-reveal>Northwind</span>
    <span class="rule-row-value">Fifteen years of orders, migrated in a weekend.</span>
  </div>
  <div class="rule-row">
    <span class="rule-row-key typewriter" x-ds-reveal>The Bird</span>
    <span class="rule-row-value">A tape machine that only ever played backwards.</span>
  </div>
</div>

A column that types itself in order

Three values that start together read as one animation. Give each a --typewriter-delay and they read as three events, in the order somebody meant them to be read. Lamp spells its own stagger the same way, and for the same reason: a row of tubes does not strike in unison.

The animation is both, which is what makes a delay safe here. forwards fills only after the run, so through its own delay a value would show its ordinary style — and the ordinary style of a typewriter is finished text. A staggered column would flash every value in full and then blank each one as its delay expired.

Scroll to it, or press replay

ROLE Director, designer
STACK Go, Raylib
SHIPPED 2026
HTML
<div class="rule-stack" style="width:100%">
  <div class="rule-row rule-row-spec">
    <span class="rule-row-key">ROLE</span>
    <span class="rule-row-value typewriter" x-ds-reveal style="--typewriter-delay:220ms">Director, designer</span>
  </div>
  <div class="rule-row rule-row-spec">
    <span class="rule-row-key">STACK</span>
    <span class="rule-row-value typewriter" x-ds-reveal style="--typewriter-delay:440ms">Go, Raylib</span>
  </div>
  <div class="rule-row rule-row-spec">
    <span class="rule-row-key">SHIPPED</span>
    <span class="rule-row-value typewriter" x-ds-reveal style="--typewriter-delay:660ms">2026</span>
  </div>
</div>

Longer titles type faster

A fixed speed per character means a thirty-character heading takes five times as long as a six-character one, and reads as slow rather than as long. A fixed duration is the opposite mistake: the long one becomes a blur. So the run is chars × speed × (baseline / chars) raised to a ratio, which is one dial between those two honest extremes.

ratio 0Every character takes speed. 2 characters 110ms, 6 → 330ms, 31 → 1705ms.
ratio 0.5The default. 2 characters 191ms, 6 → 330ms, 31 → 750ms — a long title still lasts longer, and each of its characters is quicker.
ratio 1Every title takes the same 330ms, whatever its length.

At the baseline length the factor is exactly 1 whatever the ratio says, which is what makes --ds-typewriter-baseline a calibration point rather than one more thing to re-tune: the length the speed was judged at keeps its judged speed, and every other length is measured against it.

Scroll to it, or press replay

Go Two characters, and each one is slow.
Flatty Six — the baseline, typed at exactly the agreed speed.
Why undo is the hardest feature Thirty-one, and each character is less than half as long.
HTML
<div class="rule-stack" style="width:100%">
  <div class="rule-row">
    <span class="rule-row-key typewriter" x-ds-reveal>Go</span>
    <span class="rule-row-value">Two characters, and each one is slow.</span>
  </div>
  <div class="rule-row">
    <span class="rule-row-key typewriter" x-ds-reveal>Flatty</span>
    <span class="rule-row-value">Six — the baseline, typed at exactly the agreed speed.</span>
  </div>
  <div class="rule-row">
    <span class="rule-row-key typewriter" x-ds-reveal>Why undo is the hardest feature</span>
    <span class="rule-row-value">Thirty-one, and each character is less than half as long.</span>
  </div>
</div>

Tuning it

Scroll to it, or press replay

Slower, thinner

HTML
<h2 class="typewriter" x-ds-reveal style="--typewriter-speed: 140ms; --typewriter-caret: 0.12em">Slower, thinner</h2>
--typewriter-speedTime per character at the baseline length. Every other length is scaled from it by the ratio.
--typewriter-ratioHow hard length pulls on the speed. 0 holds the speed, 1 holds the total, and the default splits the difference.
--typewriter-caretThe cursor's width. It does not blink: a blinking caret is transparent half the time, and the mask runs exactly one caret past the carriage for the caret to cover — so a blink would show the sliver through itself.

It wraps, and it still types

A heading that fits on one line at a desk is two or three on a phone, so "one line only" was never a contract anybody could keep. The mask is two layers composited: one covers every line already finished, the other is a single line box sitting on the line being typed, open as far as the carriage has gone. Added together they make a staircase, which is what typing across a wrapped heading looks like.

No element is split to do it. The obvious way to type across lines is a span per character, and it costs a fragmented accessibility tree, hundreds of extra nodes on a page of headings, and a second copy of the text as an aria-label to repair the first cost. Here the text stays one node. What the directive contributes is the two things CSS cannot work out — --ds-reveal-length and --ds-reveal-lines — and the line count is re-measured on resize, because where a browser breaks a line changes under the reader.

Scroll to it, or press replay

Why undo is the hardest feature in any editor

HTML
<h2 class="typewriter" x-ds-reveal style="width: min(100%, 330px)">
  Why undo is the hardest feature in any editor
</h2>

A heading that wraps a link counts the same, and that is worth a specimen of its own rather than a sentence. The browser reports a rectangle for the line box and one for the text inside it — four pixels apart on one line — so counting distinct tops made a six-letter title two lines deep and spent half its run revealing nothing. Nothing caught it, because every specimen here was a bare heading and every real one on a site is a link.

Scroll to it, or press replay

HTML
<h2 class="work-entry-title typewriter" x-ds-reveal>
  <a href="#typewriter">A title that is also a link</a>
</h2>

Reference

Read from components/typewriter.css when this page was built, so it cannot fall behind the stylesheet.

CLASSES.typewriter
SETTINGSSet the first on one element; a theme sets the second for all of them.--typewriter-speed — falls back to --ds-typewriter-speed--typewriter-ratio — falls back to --ds-typewriter-ratio--typewriter-caret — falls back to --ds-typewriter-caret
READS--ds-reveal-length--ds-reveal-lines--ds-typewriter-baseline

Notes

What it is not for

The last line sweeps across the whole box rather than across its own words, so a heading ending in one short word spends a beat of that line's time on empty space. Every line but the last is usually full, which is why this is worth the mechanism staying in CSS. And the same rule as the lamp applies: never on whatever a page's largest contentful element happens to be.