Efek mesin tik adalah padanan digital dari melihat seseorang berpikir keras. Teks muncul satu per satu karakter, seolah-olah tangan sungguhan sedang menekan tombol keyboard yang nyata. Anda dapat melihatnya di bagian hero situs portofolio, di emulator terminal berbasis browser, dan di jendela obrolan asisten AI yang ingin membuktikan bahwa mereka sedang "mengetik" jawaban, bukan sekadar mengambil blok teks yang sudah jadi. Jika dilakukan dengan baik, efek ini menciptakan antisipasi. Jika dilakukan dengan buruk, rasanya seperti printer yang macet di tahun 1987.

Mengapa Pola Ini Bertahan

Komputer menyampaikan informasi secara instan. Manusia tidak. Celah antara kedua kecepatan tersebut sangat berguna. Efek mesin tik menjembataninya dengan mensimulasikan ritme manusia. Pada sebuah landing page, efek ini dapat menarik mata menyusuri headline kata demi kata sehingga pengunjung benar-benar membaca proposisi nilai, bukan sekadar memindai sekilas. Dalam emulator terminal, efek ini memberikan ilusi bahwa perintah sedang dieksekusi secara real-time. Dalam antarmuka chatbot, ritmenya menandakan bahwa respons sedang dibuat secara langsung, bukan sekadar diambil dari database.

Namun, efek ini hanya akan berhasil jika mekanismenya menghormati pengguna. Detak metronomik yang datar dan monoton dengan interval yang identik akan terasa robotik. Lebih buruk lagi, implementasi yang mengabaikan teknologi asistif dapat mengubah hiasan visual yang menyenangkan menjadi penghalang yang membuat frustrasi. Tujuannya bukan untuk memperlambat pengguna; tujuannya adalah untuk menambahkan sedikit friksi agar antarmuka terasa lebih hidup.

Bangun dengan Recursive setTimeout

Mulailah dengan setTimeout, dan jangan menyentuh setInterval. Perbedaannya lebih penting daripada kelihatannya.

setInterval itu keras kepala. Ia akan berjalan setiap n milidetik terlepas dari apa pun yang terjadi di skrip Anda atau di main thread browser. Jika logika Anda membutuhkan jeda 50 milidetik di antara ketukan tombol biasa, tetapi jeda 150 milidetik setelah tanda baca, setInterval tidak dapat beradaptasi. Anda akhirnya akan membungkusnya dengan logika kondisional tambahan, berjuang melawan race conditions, dan akhirnya membersihkan serta mengatur ulang interval begitu sering sehingga kode tersebut menjadi mimpi buruk manajemen status (state management). Interval tetap akan gagal saat Anda membutuhkan kecepatan yang bervariasi.

Recursive setTimeout memperbaiki hal ini dengan membiarkan setiap langkah menentukan aturan untuk langkah berikutnya. Anggaplah ini sebagai sebuah state machine kecil. Anda mempertahankan beberapa variabel: string saat ini, indeks karakter saat ini, flag boolean untuk menentukan apakah Anda sedang mengetik atau menghapus, dan penghitung textIndex agar Anda dapat melakukan loop melalui beberapa string. Fungsi tersebut menambahkan satu karakter, memeriksa posisinya dalam kalimat, lalu menjadwalkan pemanggilan berikutnya dengan penundaan yang sesuai dengan konteksnya.

Sebagai contoh, Anda mungkin mengetik sebagian besar karakter dalam 50 milidetik, melambat menjadi 150 milidetik setelah tanda koma, dan berhenti selama 800 milidetik di akhir kalimat lengkap sebelum beralih ke mode hapus. Anda tidak dapat melakukan itu dengan rapi menggunakan setInterval. Dengan recursive setTimeout, logikanya sangat 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 membuat proses pembersihan menjadi sangat mudah. Simpan ID timeout tersebut. Saat komponen unmount atau pengguna berpindah halaman, panggil clearTimeout satu kali. Tidak akan ada interval yatim piatu yang terus berdetak di latar belakang.

Kursor Harus Berkedip Sendiri

Kursor yang berkedip adalah detail visual, bukan masalah data. Jauhkan hal ini dari mesin status JavaScript Anda. Gunakan animasi CSS terpisah yang ditempelkan pada pseudo-element ::after atau elemen <span> khusus yang terletak di akhir kontainer teks Anda.

Penggunaan @keyframes blink sederhana yang mengubah opacity atau border-color dengan timing step-end akan memberi Anda denyut yang tajam dan ramah perangkat keras yang berjalan pada compositor. JavaScript tidak perlu mengatur visibilitas kursor secara mikro. Jika Anda mengubah properti tampilan dari dalam rekursi setTimeout, Anda akan memaksa kalkulasi ulang gaya (style recalculations) yang tidak perlu pada setiap karakter. Biarkan CSS menangani estetika. Biarkan JavaScript menangani urutan.

Melakukan loop melalui beberapa string sangatlah mudah dengan penghitung textIndex. Simpan string Anda dalam sebuah array. Ketika animasi selesai pada fase penghapusan dan kontainer sudah kosong, tingkatkan textIndex menggunakan modulo panjang array, atur ulang penunjuk karakter ke nol, dan mulai mengetik lagi. Inilah cara situs portofolio melakukan siklus peran—["Developer", "Designer", "Writer"]—tanpa perlu memuat ulang halaman.

Kesalahan yang Menghancurkan Ilusi

Tiga kesalahan muncul berulang kali dalam implementasi amatir.

Menggunakan setInterval. Kita sudah membahas masalah kecepatan variabel, tetapi ada masalah yang lebih terselubung. Jika pembaruan DOM Anda mengalami keterlambatan—misalnya, karena browser sedang melakukan painting pada pergeseran tata letak (layout shift)—setInterval akan terus berjalan. Anda bisa berakhir dengan penulisan yang tumpang tindih, karakter duplikat, atau penulisan yang terjadi lebih cepat daripada kemampuan browser untuk merendernya. setTimeout rekursif menunggu hingga langkah saat ini selesai sebelum memikirkan langkah berikutnya.

Lupa melakukan escape pada HTML. Jika string sumber Anda mengandung tanda kurung sudut, dan Anda menyuntikkan konten melalui innerHTML satu per satu karakter, Anda akan membelah tag menjadi dua. Browser akan melihat <, lalu <s, lalu <st. Hal ini mencegah penguraian (parsing) tag yang benar dan dapat menyebabkan node DOM yang rusak atau kaskade gaya yang tidak terduga. Jika Anda ingin karakter literal tersebut muncul, lakukan escape terlebih dahulu atau, lebih baik lagi, tulis ke textContent alih-alih innerHTML. Jika Anda benar-benar membutuhkan span bergaya di dalam output mesin tik, lakukan proses awal (pre-process) pada string tersebut sehingga Anda tahu persis di mana tag dimulai dan berakhir sebelum memulai loop karakter.

Mengabaikan aksesibilitas. Pembaca layar (screen readers) tidak suka dibacakan teks satu per satu huruf. Saat skrip Anda menambahkan setiap karakter baru ke DOM, beberapa teknologi asistif akan mengumumkan seluruh node lagi, menyebabkan rentetan kata parsial yang terputus-putus (staccato). Itu adalah mimpi buruk bagi siapa pun yang mengandalkan navigasi auditori. Solusinya tidak rumit: tambahkan aria-label ke kontainer yang menampung teks lengkap dan final. Anda juga dapat menyembunyikan elemen animasi sepenuhnya dari teknologi asistif dengan aria-hidden="true" dan menyediakan salinan statis yang tersembunyi secara visual untuk pembaca layar. Apa pun caranya, berikan pengguna kalimat lengkap di awal daripada memaksa mereka menunggu seluruh pertunjukan Anda.

Cara Mempercantik Versi Anda

Setelah loop inti berjalan dengan baik, Anda dapat menambahkan fitur tambahan. Namun, tahan keinginan untuk menambahkannya sampai hal-hal mendasar sudah solid.

Efek suara ketikan. Klik halus pada setiap karakter bisa terasa memuaskan, tetapi audio pada halaman web adalah ranjau. Gunakan Web Audio API atau elemen Audio yang ringan dengan buffer pendek. Variasikan laju pemutaran (playback rate) sedikit—antara 0,95 dan 1,05—agar klik yang identik tidak terdengar sintetis. Selalu hormati kebijakan autoplay browser dan sediakan tombol bisu (mute toggle). Tidak ada yang membuat pengguna pergi lebih cepat daripada halaman portofolio yang memutar suara mengetik secara otomatis pada jam 9 pagi.

Pengetikan multi-baris. Jendela terminal asli melakukan pembungkusan (wrap). Jika teks Anda melewati pemutusan baris, kursor border-right yang sederhana akan melompat dengan canggung kecuali tata letak Anda dapat diprediksi. Pisahkan string berdasarkan karakter baris baru dan render setiap baris dalam <span miliknya sendiri, atau gunakan elemen pseudo yang diposisikan untuk melacak akhir konten. Berhati-hatilah dengan pembungkusan teks; kursor yang diimplementasikan sebagai inline border dapat terlepas dari teks jika lebar kontainer bergeser. Pertimbangkan untuk menggunakan white-space: pre-wrap dan font monospaced untuk gaya terminal, karena karakter dengan lebar tetap membuat perhitungan kursor jauh lebih mudah diprediksi.

Rendering Markdown secara real-time. Di sinilah segalanya menjadi rumit. Jika Anda mengetik **bold**, Anda punya pilihan: merender asterisk secara literal sebagaimana adanya, atau mengubahnya menjadi gaya tebal secara langsung (on the fly). Jika Anda memilih yang terakhir, beralih dari textContent ke innerHTML di tengah proses berarti batas node teks Anda berubah. Posisi kursor menjadi masalah pengelolaan data yang memusingkan karena tag HTML menggeser pohon DOM di bawah Anda. Satu pendekatan yang lebih aman adalah mengetik string Markdown mentah secara normal, lalu memicu proses render setelah string lengkap muncul di layar. Jika Anda benar-benar membutuhkan pemformatan langsung, pertahankan dua lapisan: buffer ketikan yang tersembunyi dan overlay visual yang telah diurai (parsed).

Efek terbalik. Menghapus teks tidak harus berarti menekan tombol backspace satu per satu karakter. Anda dapat mensimulasikan reset "pilih semua, lalu hapus" yang membersihkan bidang secara instan sebelum string berikutnya diketik. Itu terasa kaku. Alternatifnya, backspacing lambat pada 30 milidetik per karakter dapat membangun ketegangan. Campurkan keduanya: hapus kesalahan ketik dengan cepat, berhenti sejenak, lalu lanjutkan menghapus dengan kecepatan normal. Variasi tersebut memberikan kesan manusiawi pada efeknya.

Intisari Sebenarnya

Efek mesin tik adalah salah satu hiasan UI yang tampak sepele di permukaan dan baru menunjukkan kompleksitasnya setelah Anda membangunnya. Mulailah dengan