Efekt maszyny do pisania to cyfrowy odpowiednik obserwowania, jak ktoś myśli na głos. Tekst pojawia się znak po znaku, jakby prawdziwa ręka uderzała w prawdziwe klawisze. Widzimy to w sekcjach hero stron portfolio, w terminalach działających w przeglądarce oraz w oknach czatu asystentów AI, którzy chcą udowodnić, że „wpisują” odpowiedź, zamiast pobierać gotowy blok tekstu. Dobrze wykonany efekt buduje napięcie. Słabo wykonany sprawia wrażenie zaciętej drukarki z 1987 roku.
Dlaczego ten wzorzec wciąż istnieje
Komputery dostarczają informacje natychmiastowo. Ludzie – nie. Luka między tymi dwiema prędkościami jest użyteczna. Efekt maszyny do pisania niweluje ją, symulując ludzkie tempo. Na stronie typu landing page może przyciągać wzrok do nagłówka słowo po słowie, dzięki czemu odwiedzający faktycznie czytają propozycję wartości, zamiast tylko ją skanować. W emulatorze terminala buduje iluzję, że polecenia są wykonywane w czasie rzeczywistym. W interfejsie czatu bot rytm sygnalizuje, że odpowiedź jest generowana na bieżąco, a nie pobierana z bazy danych.
Jednak efekt ten działa tylko wtedy, gdy mechanika szanuje użytkownika. Płaskie, metronomiczne „tik-tik-tik” identycznych odstępów brzmi robotycznie. Co gorsza, implementacja ignorująca technologie wspomagające może zmienić zabawny wizualny akcent w frustrującą barierę. Celem nie jest spowalnianie użytkownika, lecz dodanie wystarczającej ilości tarcia, aby interfejs wydawał się żywy.
Zbuduj to za pomocą rekurencyjnego setTimeout
Zacznij od setTimeout i nie dotykaj setInterval. Różnica jest ważniejsza, niż się wydaje.
setInterval jest uparty. Wywołuje się co n milisekund, niezależnie od tego, co dzieje się w skrypcie lub głównym wątku przeglądarki. Jeśli Twoja logika wymaga 50-milisekundowej przerwy między zwykłymi uderzeniami klawiszy, ale 150-milisekundowej przerwy po znaku interpunkcyjnym, setInterval nie potrafi się dostosować. Kończysz, owijając go w dodatkową logikę warunkową, walcząc z wyścigami (race conditions) i ostatecznie tak często czyszcząc i resetując interwał, że kod staje się koszmarem zarządzania stanem. Stałe interwały zawodzą w momencie, gdy potrzebujesz zmiennych prędkości.
Rekurencyjny setTimeout rozwiązuje ten problem, pozwalając każdemu krokowi decydować o zasadach dla następnego kroku. Potraktuj to jako małą maszynę stanów. Utrzymujesz kilka zmiennych: aktualny ciąg znaków, aktualny indeks znaku, flagę boolean określającą, czy piszesz, czy usuwasz, oraz licznik textIndex, aby móc przechodzić przez wiele ciągów. Funkcja dopisuje jeden znak, sprawdza, w którym miejscu zdania się znajduje, a następnie planuje swoje kolejne wywołanie z opóźnieniem dopasowanym do kontekstu.
Na przykład większość znaków możesz wpisywać w 50 milisekund, zwolnić do 150 milisekund po przecinku i zatrzymać się na 800 milisekund na końcu pełnego zdania przed przejściem w tryb usuwania. Nie zrobisz tego czysto za pomocą setInterval. Przy rekurencyjnym setTimeout logika jest prosta:
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
Taka struktura sprawia również, że czyszczenie jest trywialne. Przechowuj ID timeoutu. Gdy komponent zostanie odmontowany lub użytkownik odejdzie z podstrony, wywołaj clearTimeout raz. Żadnych osieroconych interwałów tykających w tle.
Kursor powinien migać samodzielnie
Migający kursor to szczegół wizualny, a nie kwestia danych. Trzymaj go z dala od silnika stanu JavaScript. Użyj oddzielnej animacji CSS przypisanej do pseudoelementu ::after lub dedykowanego znacznika <span> umieszczonego na końcu kontenera tekstu.
Proste @keyframes blink przełączające opacity lub border-color z czasem step-end zapewnia wyraźne, przyjazne dla sprzętu pulsowanie, które działa na kompozytorze. JavaScript nie powinien zajmować się mikrozarządzaniem widocznością kursora. Jeśli będziesz przełączać właściwości wyświetlania wewnątrz rekurencji setTimeout, wymusisz niepotrzebne przeliczanie stylów przy każdym znaku. Pozwól CSS obsługiwać estetykę. Pozwól JavaScript obsługiwać sekwencję.
Przechodzenie przez wiele ciągów znaków jest proste dzięki licznikowi textIndex. Przechowuj swoje ciągi w tablicy. Gdy animacja zakończy fazę usuwania, a kontener będzie pusty, zwiększ textIndex modulo długością tablicy, zresetuj wskaźnik znaku do zera i zacznij pisać ponownie. W ten sposób strony portfolio przełączają się między rolami — ["Developer", "Designer", "Writer"] — bez przeładowywania strony.
Błędy, które niszczą iluzję
Trzy błędy pojawiają się raz za razem w amatorskich implementacjach.
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
