Gejalanya bikin saya garuk-garuk kepala selama setengah jam. Di sebuah custom theme klien, ada panel filter yang harusnya muncul waktu tombol ditekan. Logikanya sederhana: tambah class .is-open ke panel, dan di CSS .is-open itu display: block. Tapi panelnya keukeuh tidak mau muncul. Tidak ada error di console, tidak ada warning, tidak ada apa-apa. Class-nya jelas-jelas nempel, tapi elemennya tetap invisible.
Markupnya kira-kira begini:
<div class="filter-panel" hidden>
<!-- isi filter -->
</div>Dan CSS yang saya kira sudah cukup:
.filter-panel.is-open {
display: block;
}JavaScript-nya cuma menambah dan menghapus class:
button.addEventListener('click', () => {
panel.classList.toggle('is-open');
});Saya buka DevTools, inspect elemennya. Class is-open ada. Tapi di panel "Computed", display-nya tetap none. Saya coba naikkan specificity, tambah !important, semuanya terasa makin kotor dan tetap tidak konsisten. Ada yang menahan elemen ini tetap tersembunyi, dan itu bukan CSS saya.
Kenapa ini terjadi
Biang keroknya adalah atribut hidden di markup itu. hidden bukan sekadar atribut kosmetik — dia adalah sinyal kuat dari HTML bahwa elemen ini "tidak relevan saat ini" dan tidak boleh ditampilkan. Browser menegakkan ini lewat user-agent stylesheet dengan aturan:
[hidden] {
display: none;
}Jadi ada dua deklarasi display yang berlaku di elemen yang sama: display: none dari UA stylesheet (karena ada atribut hidden), dan display: block dari class .is-open saya. Yang bikin saya keliru, secara teori specificity class harusnya menang dari pseudo-aturan UA. Tapi praktiknya, mengandalkan class untuk meng-override [hidden] itu rapuh. Niat semantik dari hidden adalah elemen tetap tersembunyi, dan menaikkan specificity untuk "memaksa" elemen yang sengaja ditandai tidak relevan supaya muncul itu melawan arus desain HTML itu sendiri.
Intinya: saya memperlakukan hidden sebagai styling hook biasa, padahal dia itu sebenarnya display: none bawaan browser dengan intent yang kuat. Selama atribut itu masih nempel di elemen, saya akan terus berperang melawan UA stylesheet dengan cara yang kotor dan tidak bisa diandalkan.
Perbaikannya
Solusinya bukan menambah CSS lagi, melainkan berhenti melawan dan kelola atributnya langsung. Kalau elemen punya makna hidden, ya cabut atributnya saat mau ditampilkan, dan pasang lagi saat mau disembunyikan. Ini pendekatan yang bersih dan semantik:
button.addEventListener('click', () => {
if (panel.hasAttribute('hidden')) {
panel.removeAttribute('hidden');
} else {
panel.setAttribute('hidden', '');
}
});Untuk menampilkan, panel.removeAttribute('hidden'). Untuk menyembunyikan, panel.setAttribute('hidden', ''). Begitu atribut hidden lepas, aturan [hidden] { display: none } dari UA tidak berlaku lagi, dan elemen kembali ke perilaku display default-nya tanpa perlu class apa pun. Tidak ada lagi perang specificity, tidak ada !important, dan bonusnya: state visibilitas ikut benar secara aksesibilitas, karena hidden memang sinyal yang dipahami assistive technology.
Kalau memang ada alasan kuat untuk tetap mengontrol visibilitas murni lewat CSS — misalnya panel ini dikendalikan state class lain di luar kendali JS yang ini — ada jalan keluar, tapi harus eksplisit menetralkan UA stylesheet dulu:
[hidden] {
display: revert;
}
.filter-panel {
display: none;
}
.filter-panel.is-open {
display: block;
}Dengan [hidden] { display: revert }, kita sengaja menghapus paksaan display: none dari atribut, lalu visibilitas digerakkan sepenuhnya oleh class kita sendiri. Tapi jujur, ini lebih banyak gerakan dan menghapus makna semantik hidden. Buat kasus saya, mencabut dan memasang atribut lewat JS jauh lebih bersih, jadi itu yang saya pakai.
Pelajaran
hidden itu bukan styling hook. Dia adalah display: none dari browser dengan intent yang kuat: "elemen ini sedang tidak relevan". Begitu kamu paham itu, debugging-nya jadi jelas — kamu bukan sedang melawan CSS-mu sendiri, kamu sedang melawan niat HTML. Jangan coba meng-out-CSS atribut hidden dengan menaikkan specificity atau menempel !important; itu rapuh dan menyalahi semantik. Toggle atributnya: removeAttribute('hidden') untuk muncul, setAttribute('hidden', '') untuk sembunyi. Lebih sedikit kode, lebih semantik, dan aksesibilitasnya gratis. Setiap kali sebuah elemen menolak muncul padahal class-mu sudah benar, hal pertama yang saya cek sekarang adalah: apakah ada atribut hidden yang menempel diam-diam di markup.
