14 CSS Typewriter Effect Designs 01 / 14
CSS Typewriter steps() Cursor
The canonical CSS-only typewriter: width animates from 0 to a fixed ch value using steps() timing, paired with a blinking border-right cursor — no JS at all.
The code
<div class="tw-01">
<div class="tw-01__stage">
<p class="tw-01__label">Frontend Developer</p>
<h1 class="tw-01__text">Hello, World.</h1>
<p class="tw-01__sub">Building beautiful interfaces with code.</p>
</div>
</div> <div class="tw-01">
<div class="tw-01__stage">
<p class="tw-01__label">Frontend Developer</p>
<h1 class="tw-01__text">Hello, World.</h1>
<p class="tw-01__sub">Building beautiful interfaces with code.</p>
</div>
</div>.tw-01, .tw-01 *, .tw-01 *::before, .tw-01 *::after { box-sizing: border-box; margin: 0; padding: 0; }
.tw-01 ::selection { background: #a78bfa; color: #0f0a1e; }
.tw-01 {
--bg: #0f0a1e;
--surface: #1a1033;
--accent: #a78bfa;
--text: #e2d9f3;
--muted: #6b5fa0;
font-family: 'Courier New', Courier, monospace;
min-height: 340px;
display: flex;
align-items: center;
justify-content: center;
padding: 48px 24px;
}
.tw-01__stage {
display: flex;
flex-direction: column;
gap: 16px;
}
.tw-01__label {
font-size: 0.75rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--accent);
opacity: 0;
animation: tw-01-fadein 0.4s 0.2s forwards;
}
.tw-01__text {
font-size: clamp(1.8rem, 5vw, 3rem);
font-weight: 700;
color: var(--text);
overflow: hidden;
white-space: nowrap;
width: 0;
border-right: 3px solid var(--accent);
animation:
tw-01-type 1.1s steps(13) 0.8s forwards,
tw-01-blink 0.75s steps(2) 0.8s infinite;
}
.tw-01__sub {
font-size: 0.95rem;
color: var(--muted);
overflow: hidden;
white-space: nowrap;
width: 0;
animation: tw-01-sub 1.5s steps(38) 2.2s forwards;
}
@keyframes tw-01-type {
from { width: 0; }
to { width: 13ch; }
}
@keyframes tw-01-sub {
from { width: 0; }
to { width: 38ch; }
}
@keyframes tw-01-blink {
0%, 100% { border-color: var(--accent); }
50% { border-color: transparent; }
}
@keyframes tw-01-fadein {
to { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.tw-01__text { width: 13ch; animation: none; border-right-color: var(--accent); }
.tw-01__sub { width: 38ch; animation: none; }
.tw-01__label { opacity: 1; animation: none; }
} .tw-01, .tw-01 *, .tw-01 *::before, .tw-01 *::after { box-sizing: border-box; margin: 0; padding: 0; }
.tw-01 ::selection { background: #a78bfa; color: #0f0a1e; }
.tw-01 {
--bg: #0f0a1e;
--surface: #1a1033;
--accent: #a78bfa;
--text: #e2d9f3;
--muted: #6b5fa0;
font-family: 'Courier New', Courier, monospace;
min-height: 340px;
display: flex;
align-items: center;
justify-content: center;
padding: 48px 24px;
}
.tw-01__stage {
display: flex;
flex-direction: column;
gap: 16px;
}
.tw-01__label {
font-size: 0.75rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--accent);
opacity: 0;
animation: tw-01-fadein 0.4s 0.2s forwards;
}
.tw-01__text {
font-size: clamp(1.8rem, 5vw, 3rem);
font-weight: 700;
color: var(--text);
overflow: hidden;
white-space: nowrap;
width: 0;
border-right: 3px solid var(--accent);
animation:
tw-01-type 1.1s steps(13) 0.8s forwards,
tw-01-blink 0.75s steps(2) 0.8s infinite;
}
.tw-01__sub {
font-size: 0.95rem;
color: var(--muted);
overflow: hidden;
white-space: nowrap;
width: 0;
animation: tw-01-sub 1.5s steps(38) 2.2s forwards;
}
@keyframes tw-01-type {
from { width: 0; }
to { width: 13ch; }
}
@keyframes tw-01-sub {
from { width: 0; }
to { width: 38ch; }
}
@keyframes tw-01-blink {
0%, 100% { border-color: var(--accent); }
50% { border-color: transparent; }
}
@keyframes tw-01-fadein {
to { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.tw-01__text { width: 13ch; animation: none; border-right-color: var(--accent); }
.tw-01__sub { width: 38ch; animation: none; }
.tw-01__label { opacity: 1; animation: none; }
}How this works
The element has overflow: hidden; white-space: nowrap and animates from width: 0 to width: 22ch using steps(22) — each step snaps the width to reveal exactly one more character. Because steps() produces discrete jumps rather than a smooth ease, the text appears to be typed in real time without any scripting.
A second border-right: 3px solid animation toggles between the accent colour and transparent at steps(2), creating the hard on/off blink of a real terminal cursor. Both animations run on the same element; the cursor animation loops infinitely while the typing animation plays once with animation-fill-mode: forwards to hold the final state.
Customize
- Match
steps(N)andwidth: Nchto the exact character count of your string — one step per character for pixel-perfect snapping. - Change cursor style from a right-border to an underline by switching to
border-bottomand removingborder-right. - Add
animation-delay: 0.5sandanimation-fill-mode: bothto pause before the text starts appearing, useful for hero section entrances. - Use
font-variant-numeric: tabular-numson numeric strings to keep character widths uniform across all digits.
Watch out for
- Proportional fonts break the ch-based character count — this technique only works reliably with monospace fonts where every glyph is exactly 1ch wide.
- If you resize the container the cursor may drift; pin the element to
width: fit-content; max-width: Nchand avoid percentage widths on the parent. - The blinking cursor is invisible in some high-contrast Windows accessibility modes — add a
forced-colors: activefallback that makes the borderButtonText.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 4+ | 4+ | 3.5+ | 4+ |
Universally supported — the oldest reliable CSS animation technique.