25 CSS Text Animations 23 / 25
CSS Cursor Typewriter Blink Animation
A JavaScript-driven typewriter that cycles through multiple phrases with cursor blink, delete speed, and per-phrase pause — a classic hero animation.
The code
<div class="ta-23">
<div class="ta-23__stage">
<p class="ta-23__prefix">We design</p>
<div class="ta-23__line">
<span class="ta-23__typed" id="ta-23-typed"></span><span class="ta-23__cursor" id="ta-23-cursor">▋</span>
</div>
</div>
</div> <div class="ta-23">
<div class="ta-23__stage">
<p class="ta-23__prefix">We design</p>
<div class="ta-23__line">
<span class="ta-23__typed" id="ta-23-typed"></span><span class="ta-23__cursor" id="ta-23-cursor">▋</span>
</div>
</div>
</div>.ta-23, .ta-23 *, .ta-23 *::before, .ta-23 *::after {
margin: 0; padding: 0; box-sizing: border-box;
}
.ta-23 ::selection { background: #7c3aed; color: #fff; }
.ta-23 {
--bg: #f5f3ff;
min-height: 100vh;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
font-family: 'Plus Jakarta Sans', 'Segoe UI', sans-serif;
}
.ta-23__stage { text-align: left; }
.ta-23__prefix {
font-size: clamp(0.9rem, 2.5vw, 1.2rem);
color: #a78bfa;
font-weight: 600;
letter-spacing: 0.05em;
margin-bottom: 0.2rem;
}
.ta-23__line {
display: flex;
align-items: center;
min-height: 1.2em;
}
.ta-23__typed {
font-size: clamp(2rem, 6vw, 3.8rem);
font-weight: 800;
color: #1e1b4b;
white-space: nowrap;
}
.ta-23__cursor {
font-size: clamp(2rem, 6vw, 3.8rem);
font-weight: 400;
color: #7c3aed;
animation: ta-23-blink 0.85s steps(1) infinite;
margin-left: 2px;
line-height: 1;
}
.ta-23__cursor.typing {
animation-play-state: paused;
}
@keyframes ta-23-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.ta-23__cursor { animation: none; opacity: 1; }
} .ta-23, .ta-23 *, .ta-23 *::before, .ta-23 *::after {
margin: 0; padding: 0; box-sizing: border-box;
}
.ta-23 ::selection { background: #7c3aed; color: #fff; }
.ta-23 {
--bg: #f5f3ff;
min-height: 100vh;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
font-family: 'Plus Jakarta Sans', 'Segoe UI', sans-serif;
}
.ta-23__stage { text-align: left; }
.ta-23__prefix {
font-size: clamp(0.9rem, 2.5vw, 1.2rem);
color: #a78bfa;
font-weight: 600;
letter-spacing: 0.05em;
margin-bottom: 0.2rem;
}
.ta-23__line {
display: flex;
align-items: center;
min-height: 1.2em;
}
.ta-23__typed {
font-size: clamp(2rem, 6vw, 3.8rem);
font-weight: 800;
color: #1e1b4b;
white-space: nowrap;
}
.ta-23__cursor {
font-size: clamp(2rem, 6vw, 3.8rem);
font-weight: 400;
color: #7c3aed;
animation: ta-23-blink 0.85s steps(1) infinite;
margin-left: 2px;
line-height: 1;
}
.ta-23__cursor.typing {
animation-play-state: paused;
}
@keyframes ta-23-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.ta-23__cursor { animation: none; opacity: 1; }
}(function() {
const typedEl = document.getElementById('ta-23-typed');
const cursorEl = document.getElementById('ta-23-cursor');
if (!typedEl || !cursorEl) return;
const phrases = ['Experiences', 'Systems', 'Interfaces', 'Futures', 'Stories'];
const typeSpeed = 75;
const deleteSpeed = 38;
const pauseDuration = 1600;
let phraseIdx = 0;
let charIdx = 0;
let deleting = false;
function tick() {
const current = phrases[phraseIdx];
if (!deleting) {
charIdx++;
typedEl.textContent = current.slice(0, charIdx);
cursorEl.classList.add('typing');
if (charIdx === current.length) {
cursorEl.classList.remove('typing');
setTimeout(() => { deleting = true; tick(); }, pauseDuration);
return;
}
setTimeout(tick, typeSpeed);
} else {
charIdx--;
typedEl.textContent = current.slice(0, charIdx);
cursorEl.classList.add('typing');
if (charIdx === 0) {
cursorEl.classList.remove('typing');
deleting = false;
phraseIdx = (phraseIdx + 1) % phrases.length;
setTimeout(tick, 300);
return;
}
setTimeout(tick, deleteSpeed);
}
}
tick();
})(); (function() {
const typedEl = document.getElementById('ta-23-typed');
const cursorEl = document.getElementById('ta-23-cursor');
if (!typedEl || !cursorEl) return;
const phrases = ['Experiences', 'Systems', 'Interfaces', 'Futures', 'Stories'];
const typeSpeed = 75;
const deleteSpeed = 38;
const pauseDuration = 1600;
let phraseIdx = 0;
let charIdx = 0;
let deleting = false;
function tick() {
const current = phrases[phraseIdx];
if (!deleting) {
charIdx++;
typedEl.textContent = current.slice(0, charIdx);
cursorEl.classList.add('typing');
if (charIdx === current.length) {
cursorEl.classList.remove('typing');
setTimeout(() => { deleting = true; tick(); }, pauseDuration);
return;
}
setTimeout(tick, typeSpeed);
} else {
charIdx--;
typedEl.textContent = current.slice(0, charIdx);
cursorEl.classList.add('typing');
if (charIdx === 0) {
cursorEl.classList.remove('typing');
deleting = false;
phraseIdx = (phraseIdx + 1) % phrases.length;
setTimeout(tick, 300);
return;
}
setTimeout(tick, deleteSpeed);
}
}
tick();
})();How this works
JavaScript maintains a state machine with four phases: typing (adding one character every typeSpeed ms using setTimeout), pause (holding the full word for a configurable delay), deleting (removing one character every deleteSpeed ms), and next (advancing the phrase index and transitioning back to typing). Each phase reschedules itself recursively, creating a continuous loop.
The blinking cursor is a span with a CSS animation (ta-23-blink) that toggles opacity between 1 and 0 using steps(1) timing for a hard digital blink. During the typing and deleting phases the cursor blink is paused via animation-play-state: paused to simulate a real terminal cursor that stops blinking while typing is active, and resumes during the pause phase.
Customize
- Edit the
phrasesarray to change what text is typed — any number of strings, any length, including spaces, punctuation, and emojis. - Adjust
typeSpeedanddeleteSpeed— slower type with faster delete (80ms/35ms) creates a satisfying asymmetric rhythm. - Change
pauseDurationto control how long each complete phrase is displayed before deleting —1200msis quick,3000mslets it be read. - Style the cursor by changing its
backgroundor switching from a block cursor to a blinking underline usingborder-bottominstead. - Add a callback when each phrase resolves to trigger other animations — like a CTA button that fades in only when the typing finishes for dramatic effect.
Watch out for
- Using
setTimeoutrecursively is more reliable thansetIntervalfor typewriter effects because each callback is scheduled after the previous one completes, preventing drift. - The cursor element must always remain in the DOM even while typing — append text as adjacent text nodes or a sibling span, never innerHTML the cursor element itself.
- Pause the animation when the tab is hidden using the Page Visibility API (
document.visibilityState) to prevent the timer from drifting when the browser deprioritizes background tabs.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| All | All | All | All |
Uses only standard setTimeout and DOM text nodes. CSS blink animation is universally supported. Works in all modern browsers.