GODRICH

9 CSS Typing Animation, One Line, Copy-Paste

A css typing animation is a text effect that types itself onto the screen letter by letter, built from a width transition, a steps() function, and a

Auto-plays · click a tile to jump to its section · all nine in one zip

Every demo above sits in a small, non-scrollable iframe that loops on its own — there is nothing to click, so treat each one as something to watch rather than press. The nine below move from the plainest mechanism to the most composed. 01 and 02 are the two building blocks almost everything else here is made of: a caret that blinks and a line that grows in fixed steps. 03 fixes the one thing 02 quietly gets wrong once real text is involved. 04 and 05 stretch the same idea across several lines and into a full type-then-delete loop. 06 and 07 are the idea-form pair, dressing the mechanism up as an AI reply and a terminal session. 08 swaps the whole technique for an SVG stroke, and 09 closes the set by making only one word in a sentence change at a time.

01Blinking caret

A caret the exact width of one character sits right after a line of already-typed text and blinks on a hard opacity step, rather than fading in and out — real text cursors snap; they don't dim. Drop it into a form placeholder that needs a little more attention, or a chat input box that is simply waiting for someone to start typing.

step-endopacity 깜빡임::after 커서
.line__caret {
  display: inline-block;
  width: clamp(8px, 2.6vw, 14px);
  height: 1.3em;
  margin-left: clamp(2px, 1vw, 6px);
  background: $color;
  animation: caret-blink $duration $easing infinite;
}

@keyframes caret-blink {
  from { opacity: 1; }
  to   { opacity: 0; }
}

02steps() typewriter

A span's width runs from 0 up to the length of its own text using steps(), so the box grows one fixed increment at a time instead of sliding smoothly — that's the difference between a typewriter and a wipe transition. It is the shape most hero headlines and landing-page taglines reach for first, since one property carries the entire effect.

steps(n)ch 단위white-space: nowrap
.type__text {
  display: inline-block;
  overflow: hidden;
  white-space: nowrap;
  width: 0;
  border-right: .12em solid $color;
  animation: type-loop $duration linear infinite;
}

@keyframes type-loop {
  0%, 6%    { width: 0; animation-timing-function: steps(10, end); }
  58%       { width: var(--full, 10ch); }
  88%       { width: var(--full, 10ch); }
  94%, 100% { width: 0; }
}

03Multi-glyph composed typing

steps() sized in ch assumes every character takes up the same slot, which breaks the moment Hangul, other CJK glyphs, or an emoji sit next to plain Latin letters — the box either clips mid-glyph or leaves a gap. A short script measures each character's real pixel width on a canvas and drives a clip-path through those exact boundaries instead, so a mixed-script product name types cleanly.

clip-path 스텝getBoundingClientRect가변폭 대응
var el = document.getElementById('gtext'), s = el.textContent;
var cv = document.createElement('canvas').getContext('2d');
cv.font = getComputedStyle(el).font;
var w = 0, cum = [0];
for (var i = 0; i < s.length; i++) {
  w += cv.measureText(s[i]).width;
  cum.push(w);
}
var kf = cum.map(function (px) {
  return { clipPath: 'inset(0 ' + (100 - px / w * 70) + '% 0 0)', offset: px / w * .7 };
});
el.animate(kf, { duration: 2000, iterations: Infinity });

04Multi-line typing

Each line owns its own steps() keyframe, and every line after the first carries an animation-delay equal to the running total of the lines above it, so line two only starts once line one has fully typed out. That queueing is what makes a code-editor intro or a stacked bio card read as one continuous typist rather than three lines racing each other.

줄마다 steps()퍼센트 구간 분리overflow: hidden
@keyframes l1-type {
  0%      { width: 0; animation-timing-function: steps(9, end); }
  10%     { width: var(--full, 9ch); }
  88%     { width: var(--full, 9ch); }
  96%, 100% { width: 0; }
}
@keyframes l2-type {
  0%, 10% { width: 0; animation-timing-function: steps(16, end); }
  34%     { width: var(--full, 16ch); }
  88%     { width: var(--full, 16ch); }
  96%, 100% { width: 0; }
}

05Delete and retype

One keyframes block moves width from 0 up to full and back down to 0 again, with a hold in between, so typing and deleting a whole sentence live inside a single animation and never touch JavaScript. It fits an empty chat room's hint text or a search bar that cycles through a few example queries while nobody is typing yet.

단일 keyframes0→100%→0hold 구간
.search__text {
  display: inline-block;
  overflow: hidden;
  white-space: nowrap;
  width: 0;
  border-right: .1em solid $color;
  animation: search-loop $duration linear infinite;
}

@keyframes search-loop {
  0%, 6%    { width: 0; animation-timing-function: steps(11, end); }
  46%       { width: var(--full, 11ch); }
  62%       { width: var(--full, 11ch); animation-timing-function: steps(11, end); }
  96%, 100% { width: 0; }
}

06AI streaming response feel

Instead of one fixed steps() interval, a loop assigns every character a random 30-90ms gap before it appears, then bakes those uneven offsets into a clip-path keyframe list — the result reads like tokens arriving from a model rather than a metronome typing. A small pulsing dot sits beside the text as a stand-in for the "still thinking" indicator most chat UIs already show.

랜덤 간격 JSsetTimeout 루프오브 커서 pulse
var el = document.getElementById('atext'), s = el.textContent, n = s.length;
var d = [], sum = 0;
for (var i = 0; i < n; i++) {
  var r = 30 + Math.random() * 60;
  d.push(r);
  sum += r;
}
var acc = 0, kf = [{ clipPath: 'inset(0 100% 0 0)', offset: 0 }];
for (var i = 0; i < n; i++) {
  acc += d[i];
  kf.push({ clipPath: 'inset(0 ' + (100 - acc / sum * 78) + '% 0 0)', offset: acc / sum * .78 });
}
el.animate(kf, { duration: 2000, iterations: Infinity });

07Terminal prompt style

A command types out after a $ prompt using the same steps() mechanism as item 02, and once that finishes a response line underneath fades in on plain opacity — two stages chained by timing alone, no JavaScript watching for the first animation to end. Drop it into a CLI product's hero section or a dev-tool feature block that wants to show, not describe, what running the tool feels like.

명령어→응답 순서monospace 터미널 폰트트래픽라이트 장식
.term__cmd {
  display: inline-block;
  overflow: hidden;
  white-space: nowrap;
  width: 0;
  border-right: .1em solid $color;
  animation: cmd-type $duration linear infinite;
}
.term__resp {
  animation: resp-fade $duration ease-out infinite;
}

@keyframes cmd-type {
  0%, 5%    { width: 0; animation-timing-function: steps(13, end); }
  45%       { width: var(--full, 13ch); }
  85%       { width: var(--full, 13ch); }
  95%, 100% { width: 0; }
}

08SVG handwriting effect

An SVG path sets stroke-dasharray to its own total length and transitions stroke-dashoffset down from that length to zero, which uncovers the line stroke by stroke exactly the way a pen would draw it — nothing here is a typed character at all. It reads best as a signature-style reveal on an invitation or a hand-lettered wordmark, somewhere a straight typewriter cursor would feel out of place.

stroke-dasharraystroke-dashoffsetpath 길이 측정
.sig__svg path {
  stroke: $color;
  stroke-width: 5;
  stroke-linecap: round;
  stroke-dasharray: 520;
  stroke-dashoffset: 520;
  animation: draw $duration $easing infinite;
}

@keyframes draw {
  0%, 6%    { stroke-dashoffset: 520; }
  55%       { stroke-dashoffset: 0; }
  80%       { stroke-dashoffset: 0; }
  96%, 100% { stroke-dashoffset: 520; }
}

09Word swap typing

Three words sit stacked in the exact same spot at width: 0, and each one only gets its own slice of the loop's timeline in which to type itself in with steps() and clear back out again, so the rest of the sentence around it never moves. It suits a hero line that swaps a single role or noun — "built for designers / developers / marketers" — without touching any of the surrounding words.

구간별 steps()단어 3개 겹쳐두기고정 문장 + 가변 단어
.swap__word--1 { animation: w1 $duration linear infinite; }
.swap__word--2 { position: absolute; left: 0; top: 0; animation: w2 $duration linear infinite; }
.swap__word--3 { position: absolute; left: 0; top: 0; animation: w3 $duration linear infinite; }

@keyframes w1 {
  0%        { width: 0; animation-timing-function: steps(4, end); }
  8%        { width: var(--full, 4ch); }
  30%       { width: var(--full, 4ch); animation-timing-function: steps(4, end); }
  38%, 100% { width: 0; }
}

Where it breaks — the trap

Five of these nine grow width toward the exact length of a piece of text, and the laziest way to write that end value is the keyword max-content instead of a hard number. Try it and the animation stops looking like typing at all. The instant either end of a width transition is a keyword rather than a length, the browser treats the whole property as discretely animatable. As a result, even a steps() timing function can't carry it smoothly — the box jumps straight from empty to full in one frame partway through the loop instead of climbing character by character. Each of those five sidesteps this the same way. A tiny script measures the element's own rendered text with a canvas context's measureText() and writes the result into a --full custom property in real pixels, so the keyframes always animate toward an actual number. Grab all nine from the archive, already built and ready to drop in, plus the react/ versions, rather than retyping nine keyframe blocks and a canvas helper by hand — the zip below opens with the eight characters you'll find sitting in this very sentence, erv8c6eb, typed exactly as shown and nowhere else on the page.

Accessibility

None of the nine keep moving once prefers-reduced-motion: reduce is set, but each settles on a different resting state rather than simply vanishing. 01's caret stops blinking and stays visible, since a static cursor still marks where the text ends. 02, 05, 07's command line, and 09's first word all lock their width to the full --full value and drop the caret border to transparent. The complete sentence just sits there instead of stalling half-typed. 04 does the same for all three lines at once. 03 and 06 skip their JavaScript animation entirely behind a matchMedia check and fall back to a plain clip-path: none, so the full string renders immediately with no script running at all. 07's response line and 08's signature path both land on fully visible with no animation left. 09's second and third words stay at width: 0 with opacity: 0, so only the first of the three ever shows. MDN documents the prefers-reduced-motion media feature and the operating-system toggles that flip it on.

@media (prefers-reduced-motion: reduce) {
  .type__text,
  .search__text,
  .term__cmd,
  .swap__word--1 {
    animation: none;
    width: var(--full);
    border-right-color: transparent;
  }
}

Every other pattern from this site's animation notes lives under the CSS category hub, and what the zip actually contains — and what it doesn't — is explained on the about page.

FAQ

How do I make a css typing animation loop instead of running once?

Wrap the typing keyframes in animation-iteration-count: infinite and give the keyframe list a hold at the end — a percentage range where width stays at its full value — before it drops back to zero and starts over. Items 02, 05, and 07 above all use that same hold-then-reset shape, just with different percentages depending on how long the pause should feel.

Can a css typing animation work across multiple lines or a full paragraph?

Yes, but not as one animation. Give each line its own element, its own steps() keyframe sized to that line's own character count, and an animation-delay equal to the total time every line above it already took. That's exactly what item 04 does. Treating a whole paragraph as a single box breaks the moment any line wraps, since steps() only knows how to count characters along one straight measurement.

Do I need JavaScript for a css typing animation, or is plain CSS enough?

Plain CSS carries seven of the nine patterns here — items 01, 02, 04, 05, 07, 08, and 09 are pure steps() or stroke-dashoffset transitions with no script involved. JavaScript only earns its place where the text itself is unpredictable: measuring real character widths for mixed-script strings in item 03, or generating the randomized per-character timing behind item 06's streamed-reply feel.

Enter the archive password

The password is inside this article. You will find it as you read.