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.

استفاده از setInterval. ما قبلاً مشکل سرعت متغیر را پوشش داده‌ایم، اما یک مسئله ظریف‌تر وجود دارد. اگر به‌روزرسانی DOM شما هرگز با تأخیر مواجه شود—مثلاً به دلیل اینکه مرورگر در حال ترسیم یک تغییر چیدمان (layout shift) است—setInterval به اجرای خود ادامه می‌دهد. این کار می‌تواند منجر به نوشتن‌های هم‌پوشان، کاراکترهای تکراری، یا نوشتن‌هایی شود که سریع‌تر از توان رندر مرورگر اتفاق می‌افتند. setTimeout بازگشتی (recursive) تا زمانی که مرحله فعلی تمام نشود، حتی به مرحله بعدی فکر نمی‌کند.

فراموش کردن escape کردن HTML. اگر رشته‌های منبع شما حاوی علامت‌های بزرگتر و کوچکتر باشند و شما محتوا را کاراکتر به کاراکتر از طریق innerHTML تزریق کنید، تگ‌ها را از وسط نصف خواهید کرد. مرورگر ابتدا < را می‌بیند، سپس <s و بعد <st. این امر مانع از تجزیه (parsing) صحیح تگ‌ها شده و می‌تواند باعث ایجاد گره‌های DOM خراب یا آبشارهای استایل‌دهی غیرمنتظره شود. اگر می‌خواهید خود کاراکترها به صورت تحت‌اللفظی ظاهر شوند، ابتدا آن‌ها را escape کنید یا بهتر از آن، به جای innerHTML از textContent استفاده کنید. اگر واقعاً به spanهای استایل‌دار در خروجی تایپ‌رایتر نیاز دارید، رشته را پیش‌پردازش کنید تا قبل از شروع حلقه کاراکترها، دقیقاً بدانید تگ‌ها کجا شروع و کجا تمام می‌شوند.

نادیده گرفتن دسترسی‌پذیری (accessibility). صفحه‌خوان‌ها (Screen readers) از اینکه کاراکتر به کاراکتر برایشان خوانده شود، خوششان نمی‌آید. همان‌طور که اسکریپت شما هر کاراکتر جدید را به DOM اضافه می‌کند، برخی از فناوری‌های کمکی کل گره را دوباره اعلام می‌کنند که باعث ایجاد رگباری از کلمات ناقص و بریده‌بریده می‌شود. این یک کابوس برای هر کسی است که به ناوبری صوتی متکی است. راه حل پیچیده نیست: یک aria-label به کانتینری که متن کامل و نهایی را نگه می‌دارد اضافه کنید. همچنین می‌توانید عنصر متحرک را با استفاده از aria-hidden="true" کاملاً از فناوری‌های کمکی پنهان کنید و یک نسخه استاتیک که بصری پنهان است را برای صفحه‌خوان‌ها فراهم کنید. در هر دو صورت، به جای اینکه کاربران را مجبور کنید تا پایان اجرای شما صبر کنند، جمله کامل را از همان ابتدا در اختیارشان بگذارید.

راه‌هایی برای صیقل دادن نسخه خودتان

وقتی حلقه اصلی به درستی کار کرد، می‌توانید ویژگی‌های اضافی را اضافه کنید. اما در برابر وسوسه اضافه کردن آن‌ها مقاومت کنید تا زمانی که اصول اولیه مستحکم شوند.

جلوه‌های صوتی ضربه کلید. یک صدای کلیک ملایم با هر کاراکتر می‌تواند رضایت‌بخش باشد، اما صدا در صفحات وب یک میدان مین است. از Web Audio API یا یک عنصر Audio سبک با یک بافر کوتاه استفاده کنید. نرخ پخش را کمی تغییر دهید—بین 0.95 و 1.05—تا کلیک‌های یکسان مصنوعی به نظر نرسند. همیشه به سیاست‌های autoplay مرورگر احترام بگذارید و یک دکمه قطع صدا (mute toggle) فراهم کنید. هیچ چیز سریع‌تر از یک صفحه پورتفولیو که در ساعت ۹ صبح صداهای تایپ کردن را بدون قطع صدا پخش می‌کند، کاربران را فراری نمی‌دهد.

تایپ چندخطی. پنجره‌های ترمینال واقعی متن را می‌پیچند (wrap). اگر متن شما از یک خط عبور کند، یک نشانگر (cursor) ساده با border-right به شکلی ناشیانه می‌پرد، مگر اینکه چیدمان شما قابل پیش‌بینی باشد. رشته‌ها را بر اساس کاراکترهای خط جدید (newline) تقسیم کنید و هر خط را در <span مخصوص خود رندر کنید، یا از یک شبه‌عنصر (pseudo-element) موقعیت‌دار استفاده کنید که انتهای محتوا را دنبال می‌کند. در مورد پیچش متن (text wrapping) مراقب باشید؛ نشانگری که به عنوان یک border خطی پیاده‌سازی شده است، اگر عرض کانتینر تغییر کند، می‌تواند از متن جدا شود. استفاده از white-space: pre-wrap و یک فونت monospaced را برای سبک‌های ترمینال در نظر بگیرید، زیرا کاراکترهای با عرض ثابت، محاسبات نشانگر را بسیار قابل پیش‌بینی‌تر می‌کنند.

رندر کردن بلادرنگ Markdown. اینجاست که کار دشوار می‌شود. اگر تایپ کنید **bold** دو راه دارید: ستاره‌ها را دقیقاً همان‌طور که ظاهر می‌شوند رندر کنید، یا آن‌ها را در حین اجرا به استایل bold تبدیل کنید. اگر دومی را انتخاب کنید، تغییر از textContent به innerHTML در میان مسیر به این معنی است که مرزهای گره متنی شما تغییر می‌کند. موقعیت نشانگر به یک دردسر حسابداری تبدیل می‌شود زیرا تگ‌های HTML درخت DOM را زیر پای شما جابجا می‌کنند. یک رویکرد ایمن‌تر این است که رشته خام Markdown را به طور عادی تایپ کنید، سپس پس از اینکه رشته کامل روی صفحه قرار گرفت، یک مرحله رندر را اجرا کنید. اگر واقعاً به قالب‌بندی زنده نیاز دارید، دو لایه را حفظ کنید: یک بافر تایپ‌شده مخفی و یک لایه بصری تجزیه‌شده (parsed visual overlay).

جلوه‌های معکوس. پاک کردن متن لزوماً به معنای پاک کردن کاراکتر به کاراکتر با backspace نیست. می‌توانید یک بازنشانی از نوع "انتخاب همه، سپس حذف" را شبیه‌سازی کنید که فیلد را بلافاصله قبل از تایپ رشته بعدی پاک می‌کند. این حالت کمی مکانیکی به نظر می‌رسد. از طرف دیگر، پاک کردن کند با سرعت ۳۰ میلی‌ثانیه برای هر کاراکتر، هیجان ایجاد می‌کند. این دو را ترکیب کنید: یک غلط تایپی را سریع با backspace پاک کنید، مکث کنید، و سپس پاک کردن را با سرعت معمولی از سر بگیرید. این تنوع، انسانیتِ اثر را القا می‌کند.

نکته اصلی

یک اثر تایپ‌رایتر یکی از آن تزئینات رابط کاربری (UI) است که در ظاهر ساده به نظر می‌رسد و پیچیدگی خود را تنها پس از ساختن آن آشکار می‌کند. شروع کنید با