Kesan mesin taip adalah padanan digital bagi melihat seseorang berfikir secara lisan. Teks muncul satu demi satu aksara, seolah-olah tangan sebenar sedang menekan kekunci sebenar. Anda boleh melihatnya dalam bahagian hero laman web portfolio, dalam emulator terminal berasaskan pelayar, dan dalam tetingkap sembang pembantu AI yang ingin membuktikan bahawa mereka sedang "menaip" jawapan dan bukannya mengambil blok teks yang telah siap dibina. Apabila dilakukan dengan baik, ia mewujudkan rasa teruja. Apabila dilakukan dengan buruk, ia terasa seperti pencetak yang tersangkut pada tahun 1987.

Mengapa Corak Ini Kekal

Komputer menyampaikan maklumat secara sekelip mata. Manusia tidak. Jurang antara kedua-dua kelajuan ini adalah berguna. Kesan mesin taip merapatkan jurang tersebut dengan mensimulasikan rentak manusia. Pada halaman pendaratan, ia boleh menarik perhatian ke arah tajuk perkataan demi perkataan supaya pelawat benar-benar membaca tawaran nilai dan bukannya sekadar membaca sepintas lalu. Dalam emulator terminal, ia memberikan ilusi bahawa arahan sedang dilaksanakan dalam masa nyata. Dalam antara muka chatbot, ritma tersebut memberi isyarat bahawa jawapan sedang dijana secara langsung dan bukannya diambil daripada pangkalan data.

Namun, kesan ini hanya berkesan jika mekaniknya menghormati pengguna. Bunyi "tik-tik-tik" metronomik yang mendatar dengan selang masa yang serupa terasa robotik. Lebih buruk lagi, pelaksanaan yang mengabaikan teknologi bantuan boleh mengubah hiasan visual yang menyeronokkan menjadi penghalang yang mengecewakan. Matlamatnya bukan untuk melambatkan pengguna; ia adalah untuk menambah sedikit geseran supaya antara muka terasa lebih hidup.

Bina Ia dengan Recursive setTimeout

Mulakan dengan setTimeout, dan jangan sentuh setInterval. Perbezaannya lebih penting daripada yang disangka.

setInterval adalah degil. Ia akan dicetuskan setiap n milisaat tanpa mengira apa yang sedang berlaku dalam skrip anda atau thread utama pelayar. Jika logik anda memerlukan jeda 50 milisaat antara tekanan kekunci biasa tetapi jeda 150 milisaat selepas tanda baca, setInterval tidak dapat menyesuaikan diri. Anda akhirnya terpaksa membungkusnya dengan logik bersyarat tambahan, bergelut dengan race conditions, dan akhirnya membersihkan serta menetapkan semula selang masa itu dengan begitu kerap sehingga kod tersebut menjadi mimpi ngeri pengurusan keadaan (state-management). Selang masa tetap akan gagal sebaik sahaja anda memerlukan kelajuan yang berubah-ubah.

setTimeout rekursif menyelesaikan masalah ini dengan membiarkan setiap langkah menentukan peraturan untuk langkah seterusnya. Anggap ia sebagai mesin keadaan (state machine) yang kecil. Anda mengekalkan beberapa pemboleh ubah: rentetan semasa, indeks aksara semasa, bendera boolean untuk sama ada anda sedang menaip atau memadam, dan pengira textIndex supaya anda boleh mengulang melalui pelbagai rentetan. Fungsi tersebut menambah satu aksara, menyemak kedudukannya dalam ayat, kemudian menjadualkan panggilan seterusnya dengan kelewatan yang sesuai dengan konteks.

Sebagai contoh, anda mungkin menaip kebanyakan aksara pada 50 milisaat, memperlahankan kepada 150 milisaat selepas koma, dan berhenti selama 800 milisaat pada akhir ayat lengkap sebelum bertukar ke mod padam. Anda tidak boleh melakukannya dengan kemas menggunakan setInterval. Dengan setTimeout rekursif, logiknya adalah jelas:

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

Struktur ini juga memudahkan proses pembersihan. Simpan ID timeout tersebut. Apabila komponen dinyahpasang (unmount) atau pengguna beralih ke halaman lain, panggil clearTimeout sekali sahaja. Tiada lagi selang masa terbiar yang berdetak di latar belakang.

Kursor Sepatutnya Berkelip dengan Sendirinya

Kursor yang berkelip adalah perincian visual, bukan kebimbangan data. Jauhkan ia daripada enjin keadaan JavaScript anda. Gunakan animasi CSS berasingan yang dipautkan pada pseudo-elemen ::after atau <span khas yang terletak di hujung bekas teks anda.

@keyframes blink yang ringkas dengan menukar opacity atau border-color menggunakan pemasaan step-end memberikan denyutan yang tajam dan mesra perkakasan yang berjalan pada kompositor. JavaScript tidak perlu menguruskan keterlihatan kursor secara mikro. Jika anda menukar sifat paparan dari dalam rekursi setTimeout anda, anda memaksa pengiraan semula gaya yang tidak perlu bagi setiap aksara. Biarkan CSS mengendalikan estetika. Biarkan JavaScript mengendalikan urutan.

Mengulang melalui pelbagai rentetan adalah mudah dengan pengira textIndex. Simpan rentetan anda dalam satu tatasusunan (array). Apabila animasi selesai fasa pemadamannya dan bekas kosong, tambahkan textIndex modulo panjang tatasusunan, tetapkan semula penunjuk aksara ke sifar, dan mula menaip semula. Beginilah cara laman web portfolio menukar peranan—["Developer", "Designer", "Writer"]—tanpa perlu memuat semula halaman.

Kesilapan yang Menghancurkan Ilusi

Tiga ralat sering muncul berulang kali dalam pelaksanaan amatur.

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