Der Schreibmaschinen-Effekt ist das digitale Äquivalent dazu, jemandem beim lauten Denken zuzusehen. Text erscheint Zeichen für Zeichen, als ob eine echte Hand echte Tasten anschlagen würde. Man sieht ihn in den Hero-Sektionen von Portfolio-Websites, in browserbasierten Terminal-Emulatoren und in den Chat-Fenstern von KI-Assistenten, die beweisen wollen, dass sie eine Antwort „tippen“, anstatt einen Block vorgefertigter Texte abzurufen. Wenn er gut gemacht ist, erzeugt er Vorfreude. Wenn er schlecht gemacht ist, fühlt er sich an wie ein Drucker, der 1987 steckengeblieben ist.
Warum dieses Muster fortbesteht
Computer liefern Informationen sofort. Menschen nicht. Die Lücke zwischen diesen beiden Geschwindigkeiten ist nützlich. Ein Schreibmaschinen-Effekt überbrückt sie, indem er das menschliche Tempo simuliert. Auf einer Landingpage kann er das Auge Wort für Wort über eine Schlagzeile führen, sodass Besucher das Wertversprechen tatsächlich lesen, anstatt es nur zu überfliegen. In einem Terminal-Emulator vermittelt er die Illusion, dass Befehle in Echtzeit ausgeführt werden. In einer Chatbot-Schnittstelle signalisiert der Rhythmus, dass eine Antwort „on the fly“ generiert wird, anstatt sie aus einer Datenbank abzurufen.
Aber der Effekt funktioniert nur, wenn die Mechanik den Nutzer respektiert. Ein flaches, metronomisches Tick-Tick-Tick mit identischen Intervallen fühlt sich roboterhaft an. Schlimmer noch: Eine Implementierung, die assistierende Technologien ignoriert, kann eine unterhaltsame visuelle Spielerei in eine frustrierende Barriere verwandeln. Das Ziel ist nicht, den Nutzer auszubremsen; es geht darum, gerade genug Reibung hinzuzufügen, damit sich die Schnittstelle lebendig anfühlt.
Umsetzung mit rekursivem setTimeout
Beginnen Sie mit setTimeout und rühren Sie setInterval nicht an. Der Unterschied ist bedeutender, als er aussieht.
setInterval ist eigensinnig. Es wird alle n Millisekunden ausgelöst, ungeachtet dessen, was sonst in Ihrem Skript oder im Main Thread des Browsers passiert. Wenn Ihre Logik eine Pause von 50 Millisekunden zwischen normalen Tastenanschlägen, aber eine Pause von 150 Millisekunden nach Satzzeichen benötigt, kann setInterval sich nicht anpassen. Man landet dabei in der Falle, es mit zusätzlicher bedingter Logik zu umgeben, gegen Race Conditions zu kämpfen und es schließlich so oft zu löschen und zurückzusetzen, dass der Code zu einem Albtraum für das State-Management wird. Feste Intervalle scheitern in dem Moment, in dem Sie variable Geschwindigkeiten benötigen.
Rekursives setTimeout löst dies, indem es jedem Schritt erlaubt, die Regeln für den nächsten Schritt festzulegen. Betrachten Sie es als eine winzige State Machine. Sie pflegen ein paar Variablen: den aktuellen String, den aktuellen Zeichenindex, ein Boolean-Flag, ob Sie gerade tippen oder löschen, und einen textIndex-Zähler, damit Sie durch mehrere Strings iterieren können. Die Funktion hängt ein Zeichen an, prüft die Position im Satz und plant dann ihren eigenen nächsten Aufruf mit einer Verzögerung, die zum Kontext passt.
Zum Beispiel könnten Sie die meisten Zeichen mit 50 Millisekunden tippen, nach einem Komma auf 150 Millisekunden verlangsamen und am Ende eines vollständigen Satzes für 800 Millisekunden pausieren, bevor Sie in den Löschmodus wechseln. Das lässt sich mit setInterval nicht sauber umsetzen. Mit rekursivem setTimeout ist die Logik simpel:
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
Diese Struktur macht auch das Aufräumen trivial. Speichern Sie die Timeout-ID. Wenn die Komponente unmounted wird oder der Nutzer navigiert, rufen Sie einmal clearTimeout auf. Keine verwaisten Intervalle, die im Hintergrund weiterlaufen.
Der Cursor sollte von selbst blinken
Das Blinken des Cursors ist ein visuelles Detail, kein Datenproblem. Halten Sie ihn aus Ihrer JavaScript-State-Engine heraus. Verwenden Sie eine separate CSS-Animation, die an ein ::after-Pseudo-Element oder ein dediziertes <span> am Ende Ihres Textcontainers gebunden ist.
Ein einfaches @keyframes blink, das die opacity oder border-color mit step-end-Timing umschaltet, liefert Ihnen einen präzisen, hardwarefreundlichen Puls, der auf dem Compositor läuft. JavaScript hat nichts damit zu tun, die Sichtbarkeit des Cursors mikrozumanagen. Wenn Sie Display-Eigenschaften innerhalb Ihrer setTimeout-Rekursion umschalten, erzwingen Sie bei jedem einzelnen Zeichen unnötige Style-Recalculations. Überlassen Sie die Ästhetik dem CSS. Überlassen Sie die Sequenz dem JavaScript.
Das Durchlaufen mehrerer Strings ist mit einem textIndex-Zähler unkompliziert. Speichern Sie Ihre Strings in einem Array. Wenn die Animation ihre Löschphase beendet hat und der Container leer ist, erhöhen Sie den textIndex modulo der Array-Länge, setzen den Zeichenpointer auf Null zurück und beginnen erneut zu tippen. So durchlaufen Portfolio-Websites ihre Rollen – ["Developer", "Designer", "Writer"] – ohne die Seite jemals neu zu laden.
Fehler, die die Illusion zerstören
Drei Fehler treten in Amateur-Implementierungen immer wieder auf.
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
