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 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.

Usando setInterval. Já cobrimos o problema da velocidade variável, mas há uma questão mais sutil. Se a atualização do seu DOM sofrer qualquer atraso — digamos, porque o navegador está renderizando uma mudança de layout — o setInterval continuará disparando. Você pode acabar com escritas sobrepostas, caracteres duplicados ou escritas que ocorrem mais rápido do que o navegador consegue renderizar. O setTimeout recursivo espera até que a etapa atual seja concluída antes mesmo de pensar na próxima.

Esquecer de escapar o HTML. Se suas strings de origem contiverem sinais de < e >, e você estiver injetando conteúdo via innerHTML um caractere por vez, você acabará dividindo as tags ao meio. O navegador vê <, depois <s, depois <st. Isso impede o parsing adequado das tags e pode deixar você com nós do DOM quebrados ou cascatas de estilo inesperadas. Se você quiser que os caracteres literais apareçam, escape-os primeiro ou, melhor ainda, escreva em textContent em vez de innerHTML. Se você realmente precisar de spans estilizados dentro da saída da máquina de escrever, pré-processe a string para saber exatamente onde as tags começam e terminam antes de iniciar o loop de caracteres.

Ignorar a acessibilidade. Leitores de tela não gostam de ser lidos uma letra por vez. À medida que seu script anexa cada novo caractere ao DOM, algumas tecnologias assistivas anunciam o nó inteiro novamente, causando uma rajada staccato de palavras parciais. Isso é um pesadelo para qualquer pessoa que dependa de navegação auditiva. A solução não é complicada: adicione um aria-label ao contêiner que contém o texto completo e final. Você também pode ocultar o elemento animado das tecnologias assistivas inteiramente com aria-hidden="true" e fornecer uma cópia estática visualmente oculta para os leitores de tela. De qualquer forma, forneça aos usuários a frase completa de imediato, em vez de forçá-los a assistir à sua performance.

Formas de Refinar sua Versão

Assim que o loop principal estiver funcionando, você pode adicionar extras. Mas resista à tentação de adicioná-los até que os fundamentos estejam sólidos.

Efeitos sonoros de digitação. Um clique sutil em cada caractere pode ser satisfatório, mas áudio em páginas web é um campo minado. Use a Web Audio API ou um elemento Audio leve com um buffer curto. Varie a taxa de reprodução ligeiramente — entre 0,95 e 1,05 — para que cliques idênticos não soem sintéticos. Sempre respeite as políticas de autoplay do navegador e forneça um botão de mudo. Nada afasta os usuários mais rápido do que uma página de portfólio sem som reproduzindo automaticamente sons de digitação às 9 da manhã.

Digitação em múltiplas linhas. Janelas de terminal reais fazem quebra de linha. Se o seu texto cruzar uma quebra de linha, um cursor simples de border-right saltará de forma estranha, a menos que seu layout seja previsível. Divida as strings por caracteres de nova linha e renderize cada linha em seu próprio <span>, ou use um pseudo-elemento posicionado que acompanhe o final do conteúdo. Cuidado com a quebra de texto; um cursor implementado como uma borda inline pode se descolar do texto se a largura do contêiner mudar. Considere usar white-space: pre-wrap e uma fonte monoespaçada para estilos de terminal, já que caracteres de largura fixa tornam o cálculo do cursor muito mais previsível.

Renderização de Markdown em tempo real. É aqui que as coisas ficam complicadas. Se você digitar **negrito**, você tem uma escolha: renderizar os asteriscos literalmente como aparecem ou convertê-los para um estilo de negrito dinamicamente. Se você escolher a segunda opção, mudar de textContent para innerHTML no meio do processo significa que os limites dos seus nós de texto mudam. A posição do cursor torna-se uma dor de cabeça de gerenciamento, pois as tags HTML deslocam a árvore do DOM abaixo de você. Uma abordagem mais segura é digitar a string Markdown bruta normalmente e, em seguida, acionar uma passagem de renderização assim que a string completa estiver na tela. Se você realmente precisar de formatação ao vivo, mantenha duas camadas: um buffer de digitação oculto e uma sobreposição visual processada.

Efeitos de reversão. Deletar texto não precisa significar apagar um caractere por vez com o backspace. Você pode simular um reset de "selecionar tudo, depois deletar" que limpa o campo instantaneamente antes que a próxima string seja digitada. Isso parece mecânico demais. Alternativamente, um backspace lento de 30 milissegundos por caractere cria tensão. Misture os dois: apague um erro de digitação rapidamente, faça uma pausa e depois retome a exclusão na velocidade normal. Essa variação transmite a humanidade do efeito.

A Grande Lição

Um efeito de máquina de escrever é um daqueles detalhes de UI que parecem triviais na superfície e revelam sua complexidade apenas depois que você o constrói. Comece com