GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

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 blinking

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 of these css text animation effects 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. Decorating the pointer instead of the sentence is the neighboring custom cursor CSS effects set, and every other way a sentence can move short of typing is what the CSS text animation effects gather.

01Blinking caret

How it works

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.

Where it fits

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.

Blinking caret
Open in a new tab

01 · Blinking caret — Measured area 0.316% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

steps(2, jump-none)opacity 깜빡임@keyframes caret-blink
.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

The css typewriter animation core

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.

Where it fits

It is the shape most hero headlines and landing-page taglines reach for first, since one property carries the entire effect.

steps() typewriter
Open in a new tab

02 · steps() typewriter — Measured area 3.509% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

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

How it works

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.

Where it fits

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.

Multi-glyph composed typing
Open in a new tab

03 · Multi-glyph composed typing — Measured area 2.801% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

clip-path 스텝measureText가변폭 대응
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

How it works

Each line owns its own steps() keyframe, and the wait comes from how long that keyframe holds width at 0 before starting (0%, 10%, 34%), so line two only starts once line one has fully typed out.

Where it fits

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.

Multi-line typing
Open in a new tab

04 · Multi-line typing — Measured area 8.264% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

줄마다 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

How it works

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.

Where it fits

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.

Delete and retype
Open in a new tab

05 · Delete and retype — Measured area 3.679% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

단일 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

How it works

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.

Where it fits

A small pulsing dot sits beside the text as a stand-in for the "still thinking" indicator most chat UIs already show.

AI streaming response feel
Open in a new tab

06 · AI streaming response feel — Measured area 2.514% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

랜덤 간격 JSel.animate오브 커서 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

How it works

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.

Where it fits

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.

Terminal prompt style
Open in a new tab

07 · Terminal prompt style — Measured area 1.057% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

명령어→응답 순서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

How it works

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.

Where it fits

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.

SVG handwriting effect
Open in a new tab

08 · SVG handwriting effect — Measured area 1.597% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

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

How it works

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.

Where it fits

It suits a hero line that swaps a single role or noun — "built for designers / developers / marketers" — without touching any of the surrounding words.

Word swap typing
Open in a new tab

09 · Word swap typing — Measured area 4.917% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

구간별 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.

Why it jumps

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.

The fix in all five

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. Pushing 07's terminal prompt into a full-screen design is the move the HUD interface kit makes.

Accessibility

What each one settles on

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.

The scripted two

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. When the sentence should arrive whole instead of character by character, that reveal side of the family lives in the CSS text animation effects for reveals.

Can a css typing animation paragraph run across multiple lines?

Yes, but not as one animation. Give each line its own element and its own steps() keyframe sized to that line's own character count, then hold width at 0 through the span of time the lines above it need. 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.