Saya sedang menyuntikkan potongan HTML ke dalam sebuah wrapper tipografi .prose-dark di sebuah editorial site yang saya port dari React ke WordPress. Salah satu heading di HTML itu butuh warna berbeda dari default, jadi saya tempel class utility seperti biasa: <h4 class="text-white">. Hasilnya nol. Heading-nya tetap memakai warna abu-abu dari prose, seolah class text-white saya tidak pernah ada. Tidak ada error di console, tidak ada warning di build — heading-nya cuma diam-diam menolak warna yang saya minta.
Begini kira-kira yang saya tulis:
<div class="prose-dark">
<h4 class="text-white">Judul yang harusnya putih</h4>
</div>Dan begini aturan prose yang sudah ada di stylesheet, yang mengatur warna semua heading di dalam wrapper:
.prose-dark h4 {
color: #9ca3af; /* abu-abu, dan dia yang menang */
}Di DevTools jelas terlihat: color: #fff dari .text-white muncul dengan garis coret. Browser sengaja mencoretnya. Aturan prose yang menang, bukan utility saya.
Kenapa ini terjadi
Ini murni soal specificity, bukan urutan deklarasi, bukan !important, bukan cascade layer. Mari hitung dua selector yang bertarung memperebutkan warna h4 yang sama.
Selector .text-white cuma satu class. Specificity-nya (0,1,0) — satu class, nol elemen.
Selector .prose-dark h4 punya satu class (.prose-dark) plus satu type selector (h4). Specificity-nya (0,1,1) — satu class, satu elemen.
Bandingkan kolom demi kolom dari kiri: ID sama-sama nol, jumlah class sama-sama satu, lalu di kolom type, 1 mengalahkan 0. Jadi (0,1,1) lebih tinggi dari (0,1,0), dan .prose-dark h4 menang telak. Type selector h4 yang kelihatannya sepele itulah yang menambah satu poin spesifisitas, dan satu poin itu sudah cukup untuk mengubur class utility saya.
Yang bikin jebakan ini licik: secara intuisi kita merasa class utility "harusnya lebih kuat" daripada aturan dasar tipografi. Padahal aturan dasar itu menargetkan elemen lewat type selector di dalam sebuah class scope, dan kombinasi itu otomatis lebih spesifik daripada class tunggal mana pun. Selama wrapper tipografi menulis aturannya sebagai .wrapper h4, .wrapper p, .wrapper a, dia membangun tembok specificity setinggi (0,1,1) untuk setiap elemen. Class utility tunggal dengan specificity (0,1,0) tidak akan pernah bisa memanjat tembok itu. Override saya bukan salah ketik — dia memang kalah secara matematis.
Perbaikannya
Solusinya bukan menaikkan specificity override saya (itu lomba senjata yang tidak ada habisnya), tapi menurunkan specificity aturan prose-nya sampai nol. CSS punya alat tepat untuk ini: pseudo-class :where(). Apa pun yang dibungkus :where() menyumbang specificity nol, sambil tetap mencocokkan elemen yang sama.
Saya bungkus type selector di aturan prose dengan :where():
.prose-dark :where(h4) {
color: #9ca3af;
}Sekarang hitung lagi. .prose-dark masih satu class, tapi :where(h4) di dalamnya menyumbang nol. Total specificity aturan prose turun jadi (0,1,0)... lalu karena :where() benar-benar memangkas kontribusi argumennya, baseline prose ini efektif berperilaku seperti aturan yang sangat gampang ditimpa. Class utility .text-white saya yang (0,1,0) kini bisa menang lewat urutan cascade — dan kalau saya butuh kepastian penuh, override apa pun dengan satu class saja sudah cukup untuk mengalahkan default.
<div class="prose-dark">
<h4 class="text-white">Sekarang benar-benar putih</h4>
</div>Tidak ada perubahan lain. Tidak ada !important, tidak ada selector tambahan, tidak ada inline style. Cukup membungkus type selector di dalam aturan prose dengan :where(), dan heading itu langsung menerima warna dari class utility saya.
Yang menarik, ini persis cara Tailwind Typography plugin menjaga dirinya tetap bisa ditimpa. Aturan default-nya ditulis dengan :where() tepat supaya class utility apa pun yang kita tempel di elemen di dalam .prose selalu menang. Saya cuma kebetulan menulis ulang wrapper prose versi sendiri dan lupa meniru trik itu — jadi tembok specificity-nya kembali berdiri.
Pelajaran
Wrapper tipografi yang menata elemen lewat type selector — .prose h4, .prose p, .prose a — diam-diam membangun tembok specificity setinggi (0,1,1) yang memblokir override dari consumer. Begitu seseorang mencoba menempel satu class utility ke elemen di dalamnya, override itu kalah tanpa suara, tanpa error, dan bikin debugging terasa seperti mengejar hantu.
Kalau Anda menulis sistem styling yang dipakai orang lain — atau dipakai diri sendiri di masa depan — bungkus selektor default-nya dengan :where() supaya menyumbang nol specificity. Dengan begitu satu class selalu cukup untuk menimpa default, dan tidak ada yang perlu lomba !important cuma untuk mengganti warna heading. Default boleh punya opini soal tampilan, tapi jangan sampai default itu lebih galak daripada niat consumer-nya.
