← Back to gallery
CSS

Typewriter Width Reveal

A wrapper track sweeps width 0 → phrase while a separate caret layer owns its own blink cadence, so typing and blink timing stay independent. Three variants: a monospace terminal prompt with steps(), an eased editorial headline on max-content, and a slow ambient ticker.

typewriterwidth-revealsteps-timingcaret-blinkch-unitsmax-contentprefers-reduced-motion

Typewriter / width-reveal / caret

Typewriter width reveal

Reimplements the typewriter reveal by animating a wrapper track width and driving the blinking caret from its own keyframe so typing cadence and blink cadence can be tuned independently. Three variants — terminal prompt (monospace + steps), editorial headline (proportional + eased), and ambient ticker (slow sweep + soft blink).

Monospace track + steady caret

Terminal Prompt

Fixed-pitch characters tie the width sweep to a whole-character grid so the caret always lands between glyphs, never halfway through one.

  • steps()
  • monospace
  • separate caret layer

Proportional type + eased flow

Editorial Headline

Proportional letters ride a max-content wrapper so the reveal stops exactly at the phrase end, then the caret eases its blink to feel less mechanical.

  • max-content
  • proportional text
  • eased reveal

Slow sweep + gentle blink

Ambient Ticker

A slower reveal and a soft blink keep a marketing strapline from pulling focus — the caret settles into a background rhythm instead of an active cursor.

  • slow sweep
  • ambient
  • low-flicker caret

Caret inspector

Terminal Prompt

  • steps()
  • monospace
  • separate caret layer

Fixed-pitch characters tie the width sweep to a whole-character grid so the caret always lands between glyphs, never halfway through one. A ch-based track lines up with 30 monospace characters; the caret is a separate block element so its blink cadence stays independent of the typing reveal.

Helped you ship something? 🐟 Send my cat a churu

.typewriter-text {
  display: inline-block;
  overflow: hidden;
  white-space: nowrap;
  width: 0;
  /* max-width caps the reveal: a ch value for monospace, max-content
     for proportional. The keyframe animates 0 → 100% because browsers
     can't smoothly interpolate to the intrinsic max-content keyword. */
  max-width: 14ch;
  animation: typewriterReveal 2.40s steps(13, end) infinite;
}

.typewriter-caret {
  animation: typewriterBlink 0.95s steps(1, end) infinite;
}

@keyframes typewriterReveal {
  0% { width: 0; }
  60%, 100% { width: 100%; }
}

@keyframes typewriterBlink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .typewriter-text, .typewriter-caret { animation: none; }
  .typewriter-text { width: 100%; }
}

How to make this

A typewriter cursor width reveal animates the text track from width 0 to a capped width while the caret blinks on a separate keyframe.

html
1<p class="typewriter-width-recipe" aria-label="system ready">  <span class="typewriter-width-recipe__track" aria-hidden="true">    <span class="typewriter-width-recipe__text">system&gt; ready</span>    <span class="typewriter-width-recipe__caret"></span>  </span></p> <style>.typewriter-width-recipe {  margin: 0;  color: rgba(234, 248, 255, .96);  font: 700 1.05rem/1.25 ui-monospace, SFMono-Regular, Menlo, monospace;}14.typewriter-width-recipe__track {  display: inline-flex;  align-items: baseline;  max-inline-size: 100%;  padding: .55rem .7rem;  border-radius: 10px;  background: rgba(14, 28, 46, .62);}.typewriter-width-recipe__text {  display: inline-block;24  overflow: hidden;  white-space: nowrap;  inline-size: 0;  max-inline-size: 14ch;28  animation:    typewriter-width-recipe-reveal 2.4s steps(13, end) infinite;30}.typewriter-width-recipe__caret {  display: inline-block;  inline-size: 2px;  block-size: 1.1em;  margin-inline-start: 2px;  background: rgba(134, 239, 255, .95);  transform: translateY(.14em);38  animation: typewriter-width-recipe-blink .95s steps(1, end) infinite;}@keyframes typewriter-width-recipe-reveal {  0% { inline-size: 0; }  60%, 100% { inline-size: 100%; }}@keyframes typewriter-width-recipe-blink {  0%, 49% { opacity: 1; }  50%, 100% { opacity: 0; }}@media (prefers-reduced-motion: reduce) {49  .typewriter-width-recipe__text,  .typewriter-width-recipe__caret { animation: none; }  .typewriter-width-recipe__text { inline-size: 100%; }}</style>

Annotated snippet

  1. Line 1The parent exposes the complete phrase once. The visual typing layer is aria-hidden so assistive technology does not hear partial text updates.
    PitfallShould typewriter text be aria-live?

    Usually no for decorative typing. Expose the complete phrase once as normal text or aria-label, and hide the animated fragments. Use aria-live only for real status updates that change meaning over time.

  2. Line 14The track is an inline-flex wrapper so text and caret stay aligned on the same baseline while the text width changes.
  3. Line 24The text span owns overflow and nowrap. The reveal changes inline-size, but the phrase itself stays one unwrapped line inside the clipped track.
    PitfallCan this work with proportional fonts?

    Yes, but ch units no longer map to glyph widths. Use max-content, a measured custom property, or an eased reveal that accepts partial glyph exposure as an editorial effect.

  4. Line 28max-inline-size caps the reveal. Monospace prompts can use ch units; proportional phrases often need max-content or a measured custom property.

    Same 1.8s loop. Coupling the caret to the text reveal animation means the caret stops blinking once the reveal completes (it just sits at the end). Separating the caret onto its own .7s blink keyframe keeps the cursor pulsing the entire time, including while the completed phrase holds — that is what reads as "still active".

    PitfallWhy separate the caret animation from the width reveal?

    Typing cadence and blink cadence are different signals. Keeping the caret as a sibling with its own keyframe lets the reveal finish and hold while the caret continues, slows, or stops independently.

  5. Line 30steps(13, end) matches the monospace character count so the edge lands between glyphs instead of slicing through them.

    Same width: 0 → 7ch reveal over 1.8s. Smooth (linear) timing slides the clip edge through every fractional pixel — letters get sliced mid-glyph and the reveal looks like a wipe. steps(7, end) holds the width at character-boundary multiples (1ch, 2ch, ...), so each frame snaps in a whole letter at a time.

    PitfallWhy use steps() for monospace typewriter reveals?

    steps() aligns the width edge to character-like increments. With monospace text and a ch cap, the reveal lands between glyphs instead of sliding smoothly through letter shapes.

  6. Line 38The caret is a sibling, not a border on the clipped text. That lets blinking continue while the reveal timing holds on the completed phrase.
    PitfallWhy separate the caret animation from the width reveal?

    Typing cadence and blink cadence are different signals. Keeping the caret as a sibling with its own keyframe lets the reveal finish and hold while the caret continues, slows, or stops independently.

  7. Line 49Reduced motion stops both typing and blink, then shows the full text. A blinking cursor can be just as distracting as the reveal itself.
    PitfallHow should reduced motion handle typewriter effects?

    Show the full phrase immediately and stop the caret blink. Do not replace the reveal with another loop; the static final text is the most predictable reduced-motion state.

Advanced

Sync the caret to the reveal phase — solid while typing, blinking only on pause

Same 2.4s cycle, same text reveal (width 0 → 7ch via steps(7, end)). BEFORE keeps the base recipe's independent caret blink (.95s loop) — the cursor flashes constantly, including mid-typing, which subtly fights the "someone is typing right now" cue. AFTER ties the caret to the reveal's own timeline: opacity stays solid through the 0-60% reveal window, then snaps on/off only during the 60-100% hold window. Result: the cursor reads as "writing" while text is appearing, and "idle blinking" only after the phrase has settled.

View explanation and full code20 lines

The base recipe runs the caret blink on its own independent loop, so the cursor flashes the whole time — including while the text is actively revealing. Real typists do not see their cursor blink while pressing keys; the blink only appears once typing pauses. Replace the caret's standalone blink keyframe with a phase-aware keyframe that shares the same 2.4s cycle as the reveal: opacity holds at 1 through the 0-60% reveal window (caret is solidly "writing"), then snaps between visible and invisible only during the 60-100% hold window (idle blink). Same caret element, same reveal mechanism — what changes is that the two keyframes are now coordinated on one shared timeline instead of running independently.

Append these rules inside the <style> block from the base snippet above.

css
/* Advanced: phase-aware caret cadence — extends the base recipe.   The caret no longer runs its own blink loop. It shares the reveal's   2.4s cycle: solid during the 0-60% reveal phase, blinking only   during the 60-100% hold phase — so the cursor matches the rhythm   of a real keystroke session (steady while typing, blinking on idle). */.typewriter-width-recipe__caret {  animation: typewriter-width-recipe-caret-phase 2.4s linear infinite;}@keyframes typewriter-width-recipe-caret-phase {  /* 0-60% : reveal phase — caret is solid */  0%, 60%   { opacity: 1; }  /* 60-100% : hold phase — blink at ~5Hz */  64%, 70%  { opacity: 0; }  74%, 80%  { opacity: 1; }  84%, 90%  { opacity: 0; }  94%, 100% { opacity: 1; }}@media (prefers-reduced-motion: reduce) {  .typewriter-width-recipe__caret { animation: none; opacity: 1; }}

Notes

Overview

The typewriter effect reveals text one character (or line) at a time. The width-reveal approach animates a wrapper’s width from 0 to its content width with steps(N, end) timing so each step lands on a character boundary. A separate caret element blinks independently so its rhythm does not couple to the typing cadence.

When to use it

Reach for typewriter reveals on hero quotes, terminal-style product surfaces, single-line CTAs that want a dramatic entrance. Skip it for long paragraphs — users will scan ahead of the animation and the effect becomes friction. Skip it for content that screen readers need to announce immediately; the live region behavior of partially-revealed text is inconsistent across AT.

How it works

Wrap the text in a fixed-width container with overflow: hidden; the inner text node has white-space: nowrap so it does not break across lines. Animate the container’s width from 0 to a value matching the full text width (typically 20ch or measured at build time). Use animation-timing-function: steps(N, end) where N is the character count, so the width increments by one character-width per step instead of sliding smoothly. The caret is a separate ::after pseudo with its own blink keyframe so its rhythm stays independent.

Production gotchas

steps(N, end) requires N to match the visible character count; miscount and the final character pops in mid-animation. For proportional fonts, ch units approximate but do not match real character widths — either use a monospace font or measure the text width with JS at mount. The container’s width animation triggers reflow on every step — for long strings this becomes noticeable; switch to a clip-path approach above ~40 characters. Animating max-width instead of width avoids one edge case where the container collapses below content width.

Accessibility

The full text is in the DOM throughout the animation, just clipped visually — screen readers announce the entire string immediately, not character-by-character. Under prefers-reduced-motion: reduce skip the width animation and reveal the text instantly; the caret blink is decorative and can stay or pause depending on preference.

References

Implementation depth

A width-based typewriter works when the text is measured in predictable units. max-content and ch units can produce a clean reveal for monospaced or controlled strings, while proportional fonts need more testing.

The caret should not be the only sign of state. Pause or remove blink under reduced motion, keep the final text selectable, and avoid long reveals that make users wait for essential copy.