25 CSS Text Animations 01 / 25
CSS Typewriter Text Animation
A classic monospaced typewriter effect driven entirely by CSS steps() timing and a blinking cursor pseudo-element — no JavaScript required.
The code
<div class="ta-01">
<div class="ta-01__stage">
<p class="ta-01__text ta-01__text--line1">Hello, World.</p>
<p class="ta-01__text ta-01__text--line2">I'm a typewriter.</p>
<p class="ta-01__text ta-01__text--line3">Pure CSS. No JS.</p>
</div>
</div> <div class="ta-01">
<div class="ta-01__stage">
<p class="ta-01__text ta-01__text--line1">Hello, World.</p>
<p class="ta-01__text ta-01__text--line2">I'm a typewriter.</p>
<p class="ta-01__text ta-01__text--line3">Pure CSS. No JS.</p>
</div>
</div>.ta-01, .ta-01 *, .ta-01 *::before, .ta-01 *::after {
margin: 0; padding: 0; box-sizing: border-box;
}
.ta-01 ::selection { background: #a78bfa; color: #fff; }
.ta-01 {
--bg: #0d0d1a;
--green: #00ff9f;
--cursor: #00ff9f;
--muted: #4ade80;
min-height: 100vh;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
font-family: 'Courier New', Courier, monospace;
padding: 2rem;
}
.ta-01__stage {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: flex-start;
}
.ta-01__text {
font-size: clamp(1.1rem, 2.5vw, 1.6rem);
color: var(--green);
white-space: nowrap;
overflow: hidden;
width: 0;
border-right: 3px solid transparent;
}
/* Line 1 */
.ta-01__text--line1 {
animation:
ta-01-type1 1.6s steps(13) 0.3s forwards,
ta-01-blink 0.75s steps(1) infinite;
}
/* Line 2 */
.ta-01__text--line2 {
animation:
ta-01-type2 1.4s steps(19) 2.2s forwards,
ta-01-blink2 0.75s steps(1) 2.2s infinite;
opacity: 0;
animation-fill-mode: forwards;
}
/* Line 3 */
.ta-01__text--line3 {
animation:
ta-01-type3 1.2s steps(18) 3.8s forwards,
ta-01-blink3 0.75s steps(1) 3.8s infinite;
opacity: 0;
animation-fill-mode: forwards;
}
@keyframes ta-01-type1 {
0% { width: 0; border-right-color: var(--cursor); opacity: 1; }
99% { border-right-color: var(--cursor); }
100% { width: 13ch; border-right-color: transparent; }
}
@keyframes ta-01-type2 {
0% { width: 0; border-right-color: var(--cursor); opacity: 1; }
99% { border-right-color: var(--cursor); }
100% { width: 19ch; border-right-color: transparent; }
}
@keyframes ta-01-type3 {
0% { width: 0; border-right-color: var(--cursor); opacity: 1; }
100% { width: 18ch; border-right-color: var(--cursor); }
}
@keyframes ta-01-blink { 50% { border-right-color: transparent; } }
@keyframes ta-01-blink2 { 50% { border-right-color: transparent; } }
@keyframes ta-01-blink3 { 50% { border-right-color: transparent; } }
@media (prefers-reduced-motion: reduce) {
.ta-01__text {
width: auto !important;
opacity: 1 !important;
animation: none !important;
border-right-color: transparent !important;
}
} .ta-01, .ta-01 *, .ta-01 *::before, .ta-01 *::after {
margin: 0; padding: 0; box-sizing: border-box;
}
.ta-01 ::selection { background: #a78bfa; color: #fff; }
.ta-01 {
--bg: #0d0d1a;
--green: #00ff9f;
--cursor: #00ff9f;
--muted: #4ade80;
min-height: 100vh;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
font-family: 'Courier New', Courier, monospace;
padding: 2rem;
}
.ta-01__stage {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: flex-start;
}
.ta-01__text {
font-size: clamp(1.1rem, 2.5vw, 1.6rem);
color: var(--green);
white-space: nowrap;
overflow: hidden;
width: 0;
border-right: 3px solid transparent;
}
/* Line 1 */
.ta-01__text--line1 {
animation:
ta-01-type1 1.6s steps(13) 0.3s forwards,
ta-01-blink 0.75s steps(1) infinite;
}
/* Line 2 */
.ta-01__text--line2 {
animation:
ta-01-type2 1.4s steps(19) 2.2s forwards,
ta-01-blink2 0.75s steps(1) 2.2s infinite;
opacity: 0;
animation-fill-mode: forwards;
}
/* Line 3 */
.ta-01__text--line3 {
animation:
ta-01-type3 1.2s steps(18) 3.8s forwards,
ta-01-blink3 0.75s steps(1) 3.8s infinite;
opacity: 0;
animation-fill-mode: forwards;
}
@keyframes ta-01-type1 {
0% { width: 0; border-right-color: var(--cursor); opacity: 1; }
99% { border-right-color: var(--cursor); }
100% { width: 13ch; border-right-color: transparent; }
}
@keyframes ta-01-type2 {
0% { width: 0; border-right-color: var(--cursor); opacity: 1; }
99% { border-right-color: var(--cursor); }
100% { width: 19ch; border-right-color: transparent; }
}
@keyframes ta-01-type3 {
0% { width: 0; border-right-color: var(--cursor); opacity: 1; }
100% { width: 18ch; border-right-color: var(--cursor); }
}
@keyframes ta-01-blink { 50% { border-right-color: transparent; } }
@keyframes ta-01-blink2 { 50% { border-right-color: transparent; } }
@keyframes ta-01-blink3 { 50% { border-right-color: transparent; } }
@media (prefers-reduced-motion: reduce) {
.ta-01__text {
width: auto !important;
opacity: 1 !important;
animation: none !important;
border-right-color: transparent !important;
}
}How this works
The typewriter illusion works by animating width from 0 to 100% with steps() timing — each step snaps to the next character position instead of easing smoothly, making text appear one character at a time. The container uses overflow: hidden and white-space: nowrap so clipped characters are invisible until the width expands to reveal them.
The blinking cursor is a ::after pseudo-element styled as a vertical bar using border-right. A separate ta-01-blink keyframe toggles opacity between 1 and 0 using steps(1) for a hard on/off flash rather than a soft fade, matching the feel of a real terminal cursor.
Customize
- Change the text by editing the HTML and matching the
steps()count to the new character count so each step equals exactly one character. - Adjust typing speed by changing
animation-durationon.ta-01__text—3sis leisurely,1.2sfeels snappy. - Switch the cursor style from a bar to a block by replacing
border-right: 3px solidwithbackground: var(--cursor)and setting an explicitwidth: 0.6ch. - Add a second line by duplicating the element with a longer
animation-delayequal to the first line's duration, creating a sequential typing effect. - Swap the monospace font from
Courier NewtoJetBrains MonoorFira Codevia Google Fonts for a modern dev-terminal aesthetic.
Watch out for
- The
steps()count must exactly match the number of characters in the text — one step too few clips the last character; too many leaves a gap before the cursor. - This technique only works reliably with monospaced fonts where every character has the same width; proportional fonts cause uneven reveals between characters.
- Setting
widthas the animated property means the element must have a fixed or known intrinsic width — percentage widths on flex children can produce unexpected results in some browsers.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 4+ | 4+ | 3.5+ | 4+ |
steps() timing is universally supported; no vendor prefixes needed in any modern browser.