Typewriter Effect — Free HTML CSS JS Snippet
Typewriter Effect · Animations · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="scene">
<p class="sub">Hi, I'm a developer who</p>
<h1>
builds
<span class="typed-wrap">
<span id="typed"></span><span class="cursor">|</span>
</span>
</h1>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #0f172a; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 20px; }
.scene { text-align: center; }
.sub { font-size: 15px; color: #475569; margin-bottom: 10px; }
h1 {
font-size: clamp(32px, 7vw, 60px);
font-weight: 800;
color: #f1f5f9;
line-height: 1.2;
letter-spacing: -1px;
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
}
.typed-wrap {
display: inline-flex;
align-items: center;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
min-width: 2ch;
}
.cursor {
-webkit-text-fill-color: #6366f1;
animation: blink 0.9s step-end infinite;
font-weight: 300;
}
@keyframes blink { 0%,100% { opacity:1; } 50% { opacity:0; } }const words = ['fast UIs', 'clean code', 'great DX', 'cool things', 'pixel-perfect apps'];
let wi = 0, ci = 0, deleting = false;
const el = document.getElementById('typed');
function tick() {
const word = words[wi];
if (!deleting) {
el.textContent = word.slice(0, ++ci);
if (ci === word.length) { deleting = true; setTimeout(tick, 1800); return; }
} else {
el.textContent = word.slice(0, --ci);
if (ci === 0) { deleting = false; wi = (wi + 1) % words.length; setTimeout(tick, 400); return; }
}
setTimeout(tick, deleting ? 60 : 100);
}
tick();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Typewriter Effect — words Array, tick() State Machine & CSS Blink Cursor

The typewriter effect types words character by character, pauses at completion, erases character by character, then moves to the next word. Combined with a CSS blinking cursor, it creates the impression of a live terminal or a human typing. Used on hero sections (see the word-flip hero) to cycle through product values, personas, or use cases without static text — for a decode-style reveal instead, see the text scramble.
The tick() state machine
tick() is a recursive function driven by setTimeout. It has two states controlled by the deleting boolean. In the typing state (!deleting): el.textContent = word.slice(0, ++ci) adds one character per tick at 80ms. When ci === word.length the word is complete — deleting = true and a 1800ms pause happens before the next tick. In the erasing state (deleting): el.textContent = word.slice(0, --ci) removes one character per tick at 50ms (faster erase than type). When ci === 0, wi = (wi + 1) % words.length advances to the next word and deleting = false.
The words array
const words = ['fast UIs', 'clean code', 'great DX', ...] — replace with your own phrases. The array loops infinitely via modulo. Shorter words erase faster; longer words have more visible typing time.
The CSS blink cursor
The .cursor element is a | character with animation: blink 0.7s step-end infinite. step-end` makes the cursor snap on/off instantly rather than fading — matching the hard blink of a real terminal cursor.
The character-by-character typing loop
type() appends one character per call using setInterval at a speed of typically 80-100ms per character. It reads from the current word in the words array at the current charIndex position. When charIndex equals the word length, the typing phase ends and a pause begins before erasing starts. erase() decrements charIndex, removing the last character each interval at a faster speed (40-50ms). When charIndex reaches 0, the word index advances and the next word begins typing.
The blinking cursor
A ::after pseudo-element on .typewriter-text with content: '|' or a separate .cursor element blinks via a CSS keyframe: @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }. The cursor animates continuously but should be paused during typing: animation-play-state: paused on the .typing class. This matches how real terminal cursors behave — blinking when waiting, solid when typing.
Infinite loop and word cycling
The words array loops: when the last word finishes typing and erasing, the word index resets to 0. The loop repeats indefinitely. To pause at specific words, add a custom pauseTime property per word and use setTimeout instead of the interval for that word's display duration.
Step by step
How to Use
- 1Watch the effect in the previewThe typewriter cycles through words, pausing 1.8s at each full word before erasing and typing the next.
- 2Update the words arrayIn the JS panel, update the words array with your own phrases — product values, user roles, or features.
- 3Change the fixed prefix textIn the HTML panel, update the static "Build" text that precedes the animated word.
- 4Change typing and erasing speedIn the JS panel, update 80 (ms per typed character) and 50 (ms per erased character) in the setTimeout calls.
- 5Change the pause durationUpdate 1800 in the setTimeout that fires when the full word is typed.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
tick() checks the deleting state. When typing: el.textContent = word.slice(0, ++ci) adds one character. When ci equals word length, deleting = true and a 1800ms timeout fires. When erasing: ci decrements until 0, then wi advances via modulo and deleting resets to false.
The setTimeout in the typing branch uses 80ms per character. The erasing branch uses 50ms. Reduce these values (e.g. 40ms/30ms) for faster animation, or increase (120ms/80ms) for slower.
Remove the animation: blink ... property from .cursor in the CSS panel. The cursor will remain visible without blinking.
Yes. Add longer strings to the words array: "build delightful web experiences" is valid. Longer strings take more time to type and erase — adjust the 1800ms pause if needed.
Remove the modulo wraparound: instead of wi = (wi + 1) % words.length, use if (wi < words.length - 1) wi++; else { deleting = false; return; } to stop after the last word.
Yes. Click "JSX" for a React component. In React, manage ci, wi, and deleting in useState. Use useEffect with a setTimeout to call tick(), storing the timeout ID in useRef for cleanup on unmount.