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();Typewriter Effect — Free HTML CSS JS Snippet
Typewriter Effect · Animations · Plain HTML, CSS & JS · Live preview
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.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Instead of tracing the timing constants by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the deleting boolean turns a single recursive tick() function into two distinct state machines sharing one code path, and why the erase speed (50ms) is deliberately faster than the type speed (100ms). It's also a good target for an optimization question — ask whether chaining setTimeout calls indefinitely could ever leak if the component unmounts mid-word, and how you'd guard against that. For extending it, have it add per-word pause durations instead of one fixed 1800ms hold, support typing full sentences with word-wrap-aware erase timing, or pause the cycle entirely when the tab is hidden using the Page Visibility API. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a multi-word typewriter effect in plain HTML, CSS, and vanilla JavaScript with no libraries, driven by a single recursive function and setTimeout — no setInterval.
Requirements:
- A words array of phrases to cycle through indefinitely, plus a current word index, a current character index, and a boolean flag for whether the effect is currently deleting or typing.
- A single tick() function that branches on the deleting flag: when not deleting, it must slice the current word up to one more character than last time and write it into the target element's text content; when the full word length is reached, it must flip to deleting mode and wait a longer pause (roughly 1800ms) before continuing.
- When deleting, tick() must slice the current word down by one character each call; when the slice reaches zero length, it must advance to the next word index using modulo wraparound so the list loops forever, flip back to typing mode, and continue after a short pause.
- Typing characters must use a slower per-character delay than deleting characters, so erasing visibly happens faster than typing.
- A separate blinking cursor element next to the typed text must blink with a CSS keyframe animation using a hard step (not a smooth fade), so it snaps on and off like a real terminal cursor.
- The whole loop must be self-starting on page load and require no user interaction to begin cycling through the words array.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
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.