The typewriter effect is the digital equivalent of watching someone think out loud. Text appears one character at a time, as though a real hand is striking real keys. You see it in the hero sections of portfolio sites, in browser-based terminal emulators, and in the chat windows of AI assistants that want to prove they are “typing” a response rather than fetching a block of prefabricated text. When done well, it creates anticipation. When done poorly, it feels like a printer jammed in 1987.
Why This Pattern Persists
Computers deliver information instantly. Humans do not. The gap between those two speeds is useful. A typewriter effect bridges it by simulating human pacing. On a landing page, it can draw the eye across a headline word by word so visitors actually read the value proposition instead of skimming. In a terminal emulator, it sells the illusion that commands are executing in real time. In a chatbot interface, the rhythm signals that a response is being generated on the fly rather than retrieved from a database.
But the effect only works if the mechanics respect the user. A flat, metronomic tick-tick-tick of identical intervals feels robotic. Worse, an implementation that ignores assistive technology can turn a fun visual flourish into a frustrating barrier. The goal is not to slow the user down; it is to add just enough friction to make the interface feel alive.
Build It with Recursive setTimeout
Start with setTimeout, and do not touch setInterval. The difference matters more than it looks.
setInterval is stubborn. It fires every n milliseconds regardless of what else is happening in your script or the browser’s main thread. If your logic needs a 50-millisecond pause between ordinary keystrokes but a 150-millisecond pause after punctuation, setInterval cannot adapt. You end up wrapping it in extra conditional logic, fighting race conditions, and eventually clearing and resetting the interval so often that the code becomes a state-management nightmare. Fixed intervals fail the moment you need variable speeds.
Recursive setTimeout fixes this by letting each step decide the rules for the next step. Think of it as a tiny state machine. You maintain a few variables: the current string, the current character index, a boolean flag for whether you are typing or deleting, and a textIndex counter so you can loop through multiple strings. The function appends one character, checks where it is in the sentence, then schedules its own next invocation with a delay that matches the context.
For example, you might type most characters at 50 milliseconds, slow to 150 milliseconds after a comma, and pause for 800 milliseconds at the end of a full sentence before switching to delete mode. You cannot do that cleanly with setInterval. With recursive setTimeout, the logic is plain:
if typing:
append next character
if at end of string:
switch to pause mode
schedule next call after 1000ms
if deleting:
remove last character
if string empty:
increment textIndex
load next string
switch to typing mode
This structure also makes cleanup trivial. Store the timeout ID. When the component unmounts or the user navigates away, call clearTimeout once. No orphaned intervals ticking in the background.
The Cursor Should Blink on Its Own
The blinking cursor is a visual detail, not a data concern. Keep it out of your JavaScript state engine. Use a separate CSS animation attached to a ::after pseudo-element or a dedicated <span> sitting at the end of your text container.
A simple @keyframes blink toggling opacity or border-color with step-end timing gives you a crisp, hardware-friendly pulse that runs on the compositor. JavaScript has no business micromanaging cursor visibility. If you toggle display properties from inside your setTimeout recursion, you force unnecessary style recalculations every single character. Let CSS handle aesthetics. Let JavaScript handle sequence.
Looping through multiple strings is straightforward with a textIndex counter. Store your strings in an array. When the animation finishes its delete phase and the container is empty, increment textIndex modulo the array length, reset the character pointer to zero, and start typing again. This is how portfolio sites cycle through roles—["Developer", "Designer", "Writer"]—without ever reloading the page.
Mistakes That Shatter the Illusion
Three errors show up again and again in amateur implementations.
Using setInterval. We have already covered the variable-speed problem, but there is a subtler issue. If your DOM update ever lags—say, because the browser is painting a layout shift—setInterval keeps firing. You can end up with overlapping writes, duplicate characters, or writes that happen faster than the browser can render them. Recursive setTimeout waits until the current step is done before it even thinks about the next one.
Forgetting to escape HTML. If your source strings contain angle brackets, and you are injecting content via innerHTML one character at a time, you will split tags in half. The browser sees <, then <s, then <st. That prevents proper tag parsing and can leave you with broken DOM nodes or unexpected styling cascades. If you want the literal characters to appear, escape them first or, better yet, write to textContent instead of innerHTML. If you genuinely need styled spans inside the typewriter output, pre-process the string so you know exactly where tags begin and end before you start the character loop.
Ignoring accessibility. Screen readers do not enjoy being read to one letter at a time. As your script appends each new character to the DOM, some assistive technologies announce the entire node again, causing a staccato barrage of partial words. That is a nightmare for anyone relying on auditory navigation. The fix is not complicated: add an aria-label to the container that holds the full, final text. You can also hide the animated element from assistive tech entirely with aria-hidden="true" and provide a visually hidden static copy for screen readers. Either way, give users the full sentence up front instead of forcing them to sit through your performance.
Ways to Polish Your Version
Once the core loop behaves, you can layer on extras. But resist the urge to add them until the fundamentals are solid.
Keystroke sound effects. A subtle click on each character can be satisfying, but audio on webpages is a minefield. Use the Web Audio API or a lightweight Audio element with a short buffer. Vary the playback rate slightly—between 0.95 and 1.05—so identical clicks do not sound synthetic. Always respect the browser’s autoplay policies and provide a mute toggle. Nothing drives users away faster than an unmuted portfolio page AutoPlaying typing sounds at 9 AM.
Multi-line typing. Real terminal windows wrap. If your text crosses a line break, a simple border-right cursor will jump awkwardly unless your layout is predictable. Split strings by newline characters and render each line in its own <span>, or use a positioned pseudo-element that tracks the end of the content. Be careful with text wrapping; a cursor implemented as an inline border can detach from the text if the container width shifts. Consider using white-space: pre-wrap and a monospaced font for terminal styles, since fixed-width characters make cursor math far more predictable.
Real-time Markdown rendering. This is where things get tricky. If you type **bold**, you have a choice: render the asterisks literally as they appear, or convert them to a bold style on the fly. If you choose the latter, switching from textContent to innerHTML mid-stream means your text node boundaries change. The cursor position becomes a bookkeeping headache because HTML tags shift the DOM tree underneath you. One safer approach is to type the raw Markdown string normally, then trigger a render pass once the full string is on screen. If you truly need live formatting, maintain two layers: a hidden typed buffer and a parsed visual overlay.
Reverse effects. Deleting text does not have to mean backspacing one character at a time. You can simulate a “select all, then delete” reset that clears the field instantly before the next string types in. That feels clinical. Alternatively, slow backspacing at 30 milliseconds per character builds tension. Mix the two: backspace through a typo quickly, pause, then resume deleting at normal speed. The variation sells the humanity of the effect.
The Real Takeaway
A typewriter effect is one of those UI flourishes that looks trivial on the surface and reveals its complexity only after you have built it. Start with
