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
<h2 class="typewriter" x-ds-reveal>Flatty</h2>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
<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
<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
<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.
speed. 2 characters 110ms, 6 → 330ms, 31 → 1705ms.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
<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
<h2 class="typewriter" x-ds-reveal style="--typewriter-speed: 140ms; --typewriter-caret: 0.12em">Slower, thinner</h2>0 holds the speed, 1 holds the total, and the default splits the difference.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
<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
<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.
.typewriter--typewriter-speed — falls back to --ds-typewriter-speed--typewriter-ratio — falls back to --ds-typewriter-ratio--typewriter-caret — falls back to --ds-typewriter-caret--ds-reveal-length--ds-reveal-lines--ds-typewriter-baselineNotes
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.