Ефект друкарської машинки — це цифровий еквівалент спостереження за тим, як хтось думає вголос. Текст з'являється по одному символу за раз, ніби справжня рука натискає на справжні клавіші. Ви бачите це в секціях hero на сайтах-портфоліо, у термінальних емуляторах у браузері та у вікнах чату ШІ-асистентів, які хочуть продемонструвати, що вони «друкують» відповідь, а не просто видають готовий блок тексту. Якщо це зроблено добре, це створює відчуття очікування. Якщо погано — це нагадує застряглий принтер із 1987 року.

Чому цей патерн зберігається

Комп'ютери передають інформацію миттєво. Люди — ні. Ця різниця в швидкостях є корисною. Ефект друкарської машинки долає цей розрив, імітуючи людський темп. На лендингу він може приковувати погляд до заголовка, слово за словом, щоб відвідувачі дійсно прочитали ціннісну пропозицію, а не просто пробіглися очима. У термінальному емуляторі він створює ілюзію того, що команди виконуються в реальному часі. В інтерфейсі чат-бота такий ритм сигналізує про те, що відповідь генерується на льоту, а не витягується з бази даних.

Але ефект працює лише тоді, коли механіка поважає користувача. Монотонне, метрономне «тік-тік-тік» однакових інтервалів здається роботоподібним. Гірше того, реалізація, яка ігнорує допоміжні технології, може перетворити приємний візуальний ефект на дратівливий бар'єр. Мета не в тому, щоб сповільнити користувача, а в тому, щоб додати рівно стільки тертя, скільки потрібно, аби інтерфейс здавався живим.

Реалізуйте це за допомогою рекурсивного setTimeout

Почніть із setTimeout і не чіпайте setInterval. Різниця важливіша, ніж здається.

setInterval — впертий. Він спрацьовує кожні n мілісекунд незалежно від того, що відбувається у вашому скрипті або в головному потоці браузера. Якщо вашій логіці потрібна пауза у 50 мілісекунд між звичайними натисканнями клавіш, але пауза у 150 мілісекунд після розділових знаків, setInterval не зможе адаптуватися. Ви закінчите тим, що обгорнете його додатковою умовною логікою, боротиметеся з race conditions і зрештою будете так часто очищувати та скидати інтервал, що код перетвориться на кошмар управління станом. Фіксовані інтервали не працюють у ту мить, коли вам потрібна змінна швидкість.

Рекурсивний setTimeout вирішує цю проблему, дозволяючи кожному кроку визначати правила для наступного. Думайте про це як про крихітний автомат станів (state machine). Ви підтримуєте кілька змінних: поточний рядок, поточний індекс символу, булевий прапорець того, чи ви друкуєте, чи видаляєте, і лічильник textIndex, щоб ви могли перебирати кілька рядків. Функція додає один символ, перевіряє, де вона знаходиться в реченні, а потім планує свій наступний виклик із затримкою, що відповідає контексту.

Наприклад, ви можете друкувати більшість символів із затримкою 50 мілісекунд, сповільнюватися до 150 мілісекунд після коми та робити паузу на 800 мілісекунд наприкінці повного речення перед переходом у режим видалення. Ви не зможете зробити це чисто за допомогою setInterval. З рекурсивним setTimeout логіка проста:

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

Така структура також робить очищення тривіальним. Зберігайте ID таймауту. Коли компонент розмонтовується, або користувач переходить на іншу сторінку, викличте clearTimeout один раз. Жодних сирітських інтервалів, що тикають у фоні.

Курсор має блимати самостійно

Блимаючий курсор — це візуальна деталь, а не питання даних. Тримайте його поза межами вашого JavaScript-двигуна станів. Використовуйте окрему CSS-анімацію, прив'язану до псевдоелемента ::after або спеціального тега <span>, розташованого в кінці вашого контейнера тексту.

Проста анімація @keyframes blink, що перемикає opacity або border-color за допомогою таймінгу step-end, забезпечує чіткий, дружній до апаратного забезпечення пульс, який виконується на композиторі. JavaScript не має справи з мікроменеджментом видимості курсору. Якщо ви перемикаєте властивості відображення всередині рекурсії setTimeout, ви змушуєте браузер робити непотрібні перерахунки стилів на кожному символі. Дозвольте CSS займатися естетикою. Дозвольте JavaScript займатися послідовністю.

Перебір кількох рядків легко реалізувати за допомогою лічильника textIndex. Зберігайте ваші рядки в масиві. Коли анімація завершує фазу видалення і контейнер стає порожнім, збільште textIndex за модулем довжини масиву, скиньте покажчик символу на нуль і почніть друкувати знову. Саме так сайти-портфоліо циклічно змінюють ролі — ["Developer", "Designer", "Writer"] — без перезавантаження сторінки.

Помилки, що руйнують ілюзію

Три помилки знову і знову зустрічаються в аматорських реалізаціях.

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