L'effetto macchina da scrivere è l'equivalente digitale di osservare qualcuno che pensa ad alta voce. Il testo appare un carattere alla volta, come se una mano reale stesse premendo tasti veri. Lo si vede nelle hero section dei siti portfolio, negli emulatori di terminale basati su browser e nelle finestre di chat degli assistenti AI che vogliono dimostrare di stare "digitando" una risposta piuttosto che recuperare un blocco di testo prefabbricato. Quando è fatto bene, crea anticipazione. Quando è fatto male, sembra una stampante inceppata nel 1987.
Perché questo pattern persiste
I computer forniscono informazioni istantaneamente. Gli esseri umani no. Il divario tra queste due velocità è utile. Un effetto macchina da scrivere lo colma simulando il ritmo umano. In una landing page, può attirare l'occhio su un titolo parola per parola, in modo che i visitatori leggano effettivamente la proposta di valore invece di scorrere velocemente. In un emulatore di terminale, vende l'illusione che i comandi vengano eseguiti in tempo reale. In un'interfaccia chatbot, il ritmo segnala che una risposta viene generata al volo piuttosto che recuperata da un database.
Ma l'effetto funziona solo se la meccanica rispetta l'utente. Un ticchettio piatto e metronomico di intervalli identici sembra robotico. Peggio ancora, un'implementazione che ignora le tecnologie assistive può trasformare un divertente abbellimento visivo in una frustrante barriera. L'obiettivo non è rallentare l'utente; è aggiungere quel tanto di attrito necessario a far sembrare l'interfaccia viva.
Costruiscilo con setTimeout ricorsivo
Inizia con setTimeout e non toccare setInterval. La differenza conta più di quanto sembri.
setInterval è testardo. Si attiva ogni n millisecondi indipendentemente da ciò che accade nel tuo script o nel thread principale del browser. Se la tua logica richiede una pausa di 50 millisecondi tra i tasti comuni ma una pausa di 150 millisecondi dopo la punteggiatura, setInterval non può adattarsi. Finirai per avvolgerlo in logiche condizionali extra, combattendo race condition e, alla fine, cancellando e resettando l'intervallo così spesso che il codice diventerà un incubo di gestione dello stato. Gli intervalli fissi falliscono nel momento in cui hai bisogno di velocità variabili.
Il setTimeout ricorsivo risolve questo problema permettendo a ogni passaggio di decidere le regole per il passaggio successivo. Pensalo come una piccola macchina a stati. Mantieni alcune variabili: la stringa corrente, l'indice del carattere corrente, un flag booleano per capire se stai scrivendo o cancellando e un contatore textIndex per poter scorrere più stringhe. La funzione aggiunge un carattere, controlla la sua posizione nella frase e poi programma la propria prossima invocazione con un ritardo che si adatti al contesto.
Ad esempio, potresti digitare la maggior parte dei caratteri a 50 millisecondi, rallentare a 150 millisecondi dopo una virgola e fare una pausa di 800 millisecondi alla fine di una frase completa prima di passare alla modalità di cancellazione. Non puoi farlo in modo pulito con setInterval. Con il setTimeout ricorsivo, la logica è semplice:
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
Questa struttura rende anche banale la pulizia. Memorizza l'ID del timeout. Quando il componente viene rimosso (unmount) o l'utente naviga altrove, chiama clearTimeout una volta sola. Niente intervalli orfani che ticchettano in background.
Il cursore dovrebbe lampeggiare da solo
Il cursore lampeggiante è un dettaglio visivo, non una questione di dati. Tienilo fuori dal tuo motore di stato JavaScript. Usa un'animazione CSS separata collegata a un pseudo-elemento ::after o a uno <span> dedicato posizionato alla fine del tuo contenitore di testo.
Un semplice @keyframes blink che alterna opacity o border-color con un timing step-end ti offre un impulso nitido e leggero per l'hardware che viene eseguito sul compositor. JavaScript non deve occuparsi della microgestione della visibilità del cursore. Se alterni le proprietà di visualizzazione dall'interno della tua ricorsione setTimeout, costringi a ricalcoli degli stili non necessari per ogni singolo carattere. Lascia che il CSS gestisca l'estetica. Lascia che JavaScript gestisca la sequenza.
Scorrere più stringhe è semplice con un contatore textIndex. Memorizza le tue stringhe in un array. Quando l'animazione termina la fase di cancellazione e il contenitore è vuoto, incrementa textIndex modulo la lunghezza dell'array, resetta il puntatore del carattere a zero e ricomincia a scrivere. È così che i siti portfolio ciclan tra i ruoli — ["Developer", "Designer", "Writer"] — senza mai ricaricare la pagina.
Errori che distruggono l'illusione
Tre errori compaiono continuamente nelle implementazioni amatoriali.
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
