14 CSS Typewriter Effect Designs 10 / 14
CSS Typewriter JS Character Injection
JavaScript injects characters one by one into the DOM at a configurable speed, enabling proportional fonts, dynamic strings, pause-on-hover, and click-to-restart — all styled with CSS.
The code
<div class="tw-10">
<div class="tw-10__card">
<div class="tw-10__prompt">claude@studio:~$</div>
<div class="tw-10__output" id="tw-10-output" data-state="typing" aria-live="polite"></div>
<button class="tw-10__restart" id="tw-10-restart">↺ Restart</button>
</div>
</div> <div class="tw-10">
<div class="tw-10__card">
<div class="tw-10__prompt">claude@studio:~$</div>
<div class="tw-10__output" id="tw-10-output" data-state="typing" aria-live="polite"></div>
<button class="tw-10__restart" id="tw-10-restart">↺ Restart</button>
</div>
</div>.tw-10, .tw-10 *, .tw-10 *::before, .tw-10 *::after { box-sizing: border-box; margin: 0; padding: 0; }
.tw-10 ::selection { background: #7c3aed; color: #f5f3ff; }
.tw-10 {
--violet: #7c3aed;
--lavender: #a78bfa;
--bg: #09050f;
--card: #120e1f;
--border: #2a1f45;
font-family: 'Courier New', monospace;
min-height: 340px;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
}
.tw-10__card {
width: 100%;
max-width: 500px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 16px;
padding: 32px 28px;
box-shadow: 0 24px 64px rgba(124,58,237,0.15);
display: flex;
flex-direction: column;
gap: 16px;
}
.tw-10__prompt {
font-size: 0.8rem;
color: var(--lavender);
opacity: 0.6;
}
.tw-10__output {
font-size: clamp(1.1rem, 3.5vw, 1.5rem);
color: #f5f3ff;
min-height: 2.4em;
line-height: 1.5;
position: relative;
}
.tw-10__output::after {
content: '';
display: inline-block;
width: 2px;
height: 1.1em;
background: var(--lavender);
vertical-align: middle;
margin-left: 2px;
animation: tw-10-blink 0.75s steps(2) infinite;
}
.tw-10__output[data-state="done"]::after {
background: var(--violet);
animation-duration: 1.2s;
}
.tw-10__restart {
align-self: flex-start;
background: transparent;
border: 1px solid var(--border);
color: var(--lavender);
font-family: inherit;
font-size: 0.8rem;
padding: 6px 14px;
border-radius: 6px;
cursor: pointer;
transition: border-color 0.2s, color 0.2s;
}
.tw-10__restart:hover { border-color: var(--lavender); color: #fff; }
@keyframes tw-10-blink {
0%,100% { opacity: 1; }
50% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.tw-10__output::after { animation: none; opacity: 1; }
} .tw-10, .tw-10 *, .tw-10 *::before, .tw-10 *::after { box-sizing: border-box; margin: 0; padding: 0; }
.tw-10 ::selection { background: #7c3aed; color: #f5f3ff; }
.tw-10 {
--violet: #7c3aed;
--lavender: #a78bfa;
--bg: #09050f;
--card: #120e1f;
--border: #2a1f45;
font-family: 'Courier New', monospace;
min-height: 340px;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
}
.tw-10__card {
width: 100%;
max-width: 500px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 16px;
padding: 32px 28px;
box-shadow: 0 24px 64px rgba(124,58,237,0.15);
display: flex;
flex-direction: column;
gap: 16px;
}
.tw-10__prompt {
font-size: 0.8rem;
color: var(--lavender);
opacity: 0.6;
}
.tw-10__output {
font-size: clamp(1.1rem, 3.5vw, 1.5rem);
color: #f5f3ff;
min-height: 2.4em;
line-height: 1.5;
position: relative;
}
.tw-10__output::after {
content: '';
display: inline-block;
width: 2px;
height: 1.1em;
background: var(--lavender);
vertical-align: middle;
margin-left: 2px;
animation: tw-10-blink 0.75s steps(2) infinite;
}
.tw-10__output[data-state="done"]::after {
background: var(--violet);
animation-duration: 1.2s;
}
.tw-10__restart {
align-self: flex-start;
background: transparent;
border: 1px solid var(--border);
color: var(--lavender);
font-family: inherit;
font-size: 0.8rem;
padding: 6px 14px;
border-radius: 6px;
cursor: pointer;
transition: border-color 0.2s, color 0.2s;
}
.tw-10__restart:hover { border-color: var(--lavender); color: #fff; }
@keyframes tw-10-blink {
0%,100% { opacity: 1; }
50% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.tw-10__output::after { animation: none; opacity: 1; }
}(function() {
const el = document.getElementById('tw-10-output');
const btn = document.getElementById('tw-10-restart');
if (!el) return;
const strings = [
'Designing with intention, building with precision.',
'Every pixel tells a story.',
'Code is craft. Make it beautiful.'
];
let strIdx = 0, charIdx = 0, timer = null;
function getDelay(char) {
if ('.!?'.includes(char)) return 520;
if (',;:'.includes(char)) return 240;
return 55 + Math.random() * 30;
}
function type() {
const str = strings[strIdx];
if (charIdx < str.length) {
el.textContent += str[charIdx];
charIdx++;
timer = setTimeout(type, getDelay(str[charIdx - 1]));
} else {
el.dataset.state = 'done';
timer = setTimeout(() => {
erase();
}, 2200);
}
}
function erase() {
const str = strings[strIdx];
if (el.textContent.length > 0) {
el.textContent = el.textContent.slice(0, -1);
el.dataset.state = 'typing';
timer = setTimeout(erase, 28);
} else {
strIdx = (strIdx + 1) % strings.length;
charIdx = 0;
timer = setTimeout(type, 400);
}
}
function restart() {
clearTimeout(timer);
el.textContent = '';
el.dataset.state = 'typing';
strIdx = 0; charIdx = 0;
timer = setTimeout(type, 300);
}
btn.addEventListener('click', restart);
restart();
})(); (function() {
const el = document.getElementById('tw-10-output');
const btn = document.getElementById('tw-10-restart');
if (!el) return;
const strings = [
'Designing with intention, building with precision.',
'Every pixel tells a story.',
'Code is craft. Make it beautiful.'
];
let strIdx = 0, charIdx = 0, timer = null;
function getDelay(char) {
if ('.!?'.includes(char)) return 520;
if (',;:'.includes(char)) return 240;
return 55 + Math.random() * 30;
}
function type() {
const str = strings[strIdx];
if (charIdx < str.length) {
el.textContent += str[charIdx];
charIdx++;
timer = setTimeout(type, getDelay(str[charIdx - 1]));
} else {
el.dataset.state = 'done';
timer = setTimeout(() => {
erase();
}, 2200);
}
}
function erase() {
const str = strings[strIdx];
if (el.textContent.length > 0) {
el.textContent = el.textContent.slice(0, -1);
el.dataset.state = 'typing';
timer = setTimeout(erase, 28);
} else {
strIdx = (strIdx + 1) % strings.length;
charIdx = 0;
timer = setTimeout(type, 400);
}
}
function restart() {
clearTimeout(timer);
el.textContent = '';
el.dataset.state = 'typing';
strIdx = 0; charIdx = 0;
timer = setTimeout(type, 300);
}
btn.addEventListener('click', restart);
restart();
})();How this works
A JS interval calls textContent += chars[index] on the target element, advancing one character per tick. The interval delay is tunable per character — punctuation like , and . trigger a longer pause by checking chars[index] and using a different timeout duration before scheduling the next character. This mirrors the natural rhythm of a human typist slowing at punctuation.
The cursor is a CSS ::after pseudo-element with a blinking border-right animation. When typing is paused (hover) or complete, the JS toggles a data-state attribute on the element — CSS attribute selectors change the cursor colour and blink speed accordingly, keeping all visual logic in the stylesheet.
Customize
- Pass an array of strings to cycle through — after each completes, run a deletion loop with
textContent = textContent.slice(0, -1)on a faster interval. - Vary the typing speed per character by mapping a probability distribution: most chars at 60ms, occasional 120ms pauses on random chars to simulate hesitation.
- Add a
IntersectionObserverto start the animation only when the element enters the viewport — ideal for long-scroll landing pages.
Watch out for
- Never use
innerHTML +=for injection — it re-parses the whole element every tick and breaks any child nodes. UsetextContentordocument.createTextNode(). - Interval-based typewriters drift over long sequences due to JS event loop jitter — for precise timing, use
requestAnimationFramewithperformance.now()delta tracking. - If the user navigates away mid-type, the interval continues running — always store the interval ID and call
clearIntervalin a cleanup function orvisibilitychangelistener.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 4+ | 4+ | 3.5+ | 4+ |
Vanilla JS with no modern API dependencies — works everywhere.