Bug ini tidak ketemu lewat error log. Dia ketemu karena ada pengunjung yang iseng mengklik tombol rentang waktu di sebuah panel grafik, lalu bilang tombolnya tidak ngapa-ngapain. Keluhannya kecil, tapi saya jadi curiga: kalau tombol itu mati, ada berapa lagi yang begitu di situs ini?
Audit hari itu menemukan sesuatu yang jauh lebih tidak enak daripada tombol mati.
Gejalanya: email pengunjung mendarat di URL bar
Homepage punya form berlangganan newsletter. Kotak input rapi, tombol SUBSCRIBE kontras, semuanya terlihat seperti fitur yang beneran jalan. Saya ketik alamat email, tekan Enter.
Halaman reload. Dan di URL bar sekarang tertulis kira-kira begini:
https://situs-klien.example/?email=nama%40domain.comAlamat email yang barusan saya ketik terpampang di URL bar, tersimpan di riwayat browser, tercatat utuh di log akses server, dan ikut terbaca oleh skrip analitik mana pun yang melaporkan URL halaman apa adanya. Tidak ada satu pun email yang benar-benar masuk ke mana-mana, tentu saja, karena tidak ada yang menerimanya. Yang terjadi cuma kebocoran satu arah.
Kenapa bisa begitu: form tanpa action itu submit ke dirinya sendiri
Ini bukan bug framework, ini perilaku bawaan HTML yang sudah begitu sejak dulu. Kalau sebuah <form> tidak punya action, browser memakai URL halaman saat ini sebagai target. Kalau tidak punya method, default-nya GET. Dan GET artinya semua field yang punya atribut name diserialisasi jadi query string.
Jadi markup yang kelihatannya tidak berbahaya ini:
<form className="flex gap-2">
<input type="email" name="email" placeholder="Email kamu" />
<button className="bg-white text-black px-4 py-2">SUBSCRIBE</button>
</form>secara efektif berarti: "kirim isi form ini ke halaman ini sendiri, lewat URL." Tombol di dalam <form> yang tidak diberi type juga default-nya type="submit", jadi klik apa pun di situ memicu submit native.
Tidak ada onSubmit, tidak ada action, tidak ada endpoint. Yang ada cuma browser yang menjalankan tugasnya dengan patuh.
Akar masalahnya: UI fase mockup yang naik produksi setengah terpasang
Form itu lahir waktu halaman masih tahap mockup. Tujuannya waktu itu cuma satu: menunjukkan seperti apa bentuk blok newsletter di layout. Wiring-nya menyusul di fase berikutnya, backend-nya juga.
Fase berikutnya belum datang, tapi halamannya sudah tayang.
Ini pola yang saya lihat berulang di proyek yang dibangun bertahap: elemen yang dibuat untuk keperluan visual ikut terbawa ke produksi tanpa pernah ditandai sebagai belum aktif. Waktu audit itu saya sisir seluruh situs dan ketemu 12 permukaan dengan penyakit yang sama, dari tombol filter rentang grafik yang bikin pengunjung ngeluh sampai form newsletter yang membocorkan email tadi. Semuanya terlihat aktif. Tidak ada satu pun yang benar-benar terhubung ke apa pun.
Yang bikin kelas bug ini mahal: dia tidak muncul di error log, tidak bikin build merah, tidak kena test. Dari sisi mesin, tidak ada yang rusak. Yang rusak cuma janji yang dibuat UI ke pengunjung.
Dua "perbaikan cepat" yang justru memperburuk
Waktu ketemu form seperti ini, refleks pertama biasanya menambal submit-nya:
<form onSubmit={(e) => e.preventDefault()}>Ini memang menghentikan kebocoran URL. Tapi hasil akhirnya lebih buruk secara pengalaman: sekarang tombolnya bisa diklik, kelihatan aktif, dan tidak melakukan apa-apa sama sekali. Tidak ada reload, tidak ada pesan, tidak ada umpan balik. Pengunjung akan mengira dia yang salah, lalu mengklik lagi tiga kali.
Refleks kedua yang sama menyesatkannya:
<a href="#">Coming soon</a>Anchor ke # tetap terbaca sebagai tautan oleh pembaca layar, tetap masuk urutan tab, dan tetap mengubah URL. Sama saja bohongnya, cuma bentuknya beda.
Dua-duanya menyembunyikan gejala tanpa memperbaiki masalah aslinya, yaitu UI yang berbohong soal statusnya sendiri.
Perbaikannya: disabled state yang jujur
Yang saya pakai untuk 12 permukaan itu adalah resep yang sama, dan kuncinya bukan CSS, tapi keputusan untuk berhenti pura-pura aktif.
<button
type="button"
disabled
title="Pendaftaran email aktif bersama Fase 3"
className="... opacity-60 cursor-not-allowed"
>
SUBSCRIBE · soon
</button>Empat bagian, masing-masing punya tugas:
type="button" melepas tombol dari perilaku submit bawaan, jadi klik di atasnya tidak lagi memicu submit native meskipun elemennya masih terbungkus <form>.
Atribut disabled adalah pekerja utamanya. Dia bukan sekadar bikin pucat: dia mengeluarkan tombol dari urutan tab, memblokir klik di level browser, dan membuat pembaca layar mengumumkan tombol itu sebagai tidak tersedia. Ini satu-satunya bagian yang bekerja untuk pengguna keyboard dan pengguna teknologi bantu, bukan cuma untuk yang melihat layar.
opacity-60 cursor-not-allowed adalah sinyal visualnya. Kursor yang berubah waktu hover itu penting karena dia menjawab pertanyaan pengunjung sebelum dia sempat mengklik.
Sufiks label · soon adalah bagian yang paling sering dilupakan. Tooltip title cuma muncul kalau kamu hover dengan mouse dan menunggu, jadi dia tidak berguna di layar sentuh dan tidak bisa diandalkan sebagai satu-satunya penjelasan. Teks di dalam label ikut terbaca oleh semua orang, termasuk pembaca layar. Tooltip-nya bonus untuk konteks tambahan, bukan penopang utamanya.
Untuk form newsletter tadi, input-nya juga ikut kena disabled, dan bagian ini bukan sekadar kerapian. Gejala di awal tadi saya picu dengan menekan Enter, bukan mengklik, jadi type="button" saja tidak cukup. HTML punya perilaku implicit submission: form yang cuma punya satu field teks tetap ikut ter-submit waktu kamu menekan Enter di field itu, walaupun di dalamnya tidak ada tombol submit sama sekali. Input yang disabled yang benar-benar menutup jalur itu, karena field mati tidak bisa difokus dan tidak ikut terkirim.
Aturan yang saya pegang sekarang
Dari audit itu saya menetapkan dua larangan keras di proyek yang dibangun bertahap:
- Jangan pernah pakai
<form onSubmit={(e) => e.preventDefault()}>di balik tombol yang tampilannya aktif. Kalau belum jalan, bilang belum jalan. - Jangan pernah pakai
<a href="#">sebagai placeholder. Kalau belum ada tujuannya, itu bukan tautan.
Dan satu kebiasaan baru: setiap kali sebuah halaman naik dari mockup ke produksi, sisir semua elemen interaktifnya satu per satu dan tanya apakah dia benar-benar terhubung ke sesuatu. Grep cepat untuk href="#", <form tanpa onSubmit, dan <button> tanpa type biasanya sudah cukup untuk memunculkan daftar tersangkanya.
Yang saya bawa pulang
- Form tanpa
actiondan tanpaonSubmitbukan form yang mati. Dia form yang submit ke dirinya sendiri lewatGET, dan isinya jadi query string yang kelihatan semua orang. - Kalau field-nya berisi data pribadi, ini bukan cuma bug UX. Query string mendarat di riwayat browser, di log akses server, dan di laporan analitik yang mencatat URL halaman apa adanya. Header
Referersendiri tidak sebocor yang sering diduga: default browser modern,strict-origin-when-cross-origin, cuma mengirim origin ke domain lain. Yang tetap menerima URL utuh adalah request satu domain dan skrip apa pun yang membacalocation.href. - Ada kelas bug yang tidak pernah muncul di log karena secara teknis tidak ada yang gagal. Satu-satunya cara menemukannya adalah dengan benar-benar memakai situsnya seperti pengunjung biasa.
- UI yang jujur soal apa yang belum siap selalu lebih baik daripada UI yang terlihat lengkap tapi diam waktu diklik. Pengunjung memaafkan fitur yang belum ada. Yang dia tidak maafkan adalah merasa dibohongi.