Selector modern

Selector modern :is(), :where(), dan :has() beserta aturan specificity masing-masing.

Daftar isi

Selector klasik menargetkan elemen berdasarkan identitas dan posisinya sendiri, sedangkan tiga selector modern memperluas jangkauan ke pengelompokan dan isi konten. Fungsi :is() merapikan daftar selector dengan bobot argumen terkuatnya, :where() menyediakan pengelompokan serupa tanpa bobot sama sekali, dan :has() memungkinkan sebuah elemen dipilih berdasarkan isinya sehingga gaya induk dapat mengikuti anaknya. Topik ini membahas ketiganya, aturan specificity masing-masing, dukungan browser, serta penerapannya pada kisi kartu.

:is() untuk pengelompokan selector#

Fungsi :is() menerima daftar selector dan mencocokkan elemen yang memenuhi salah satu argumennya, sehingga pengulangan pada selector majemuk dapat diringkas. Perbandingan berikut memperlihatkan penulisan lama dan bentuk ringkasnya untuk gaya tautan pada dua area berbeda.

css Contoh: pengulangan selector tanpa :is()
nav a:hover,
nav a:focus,
footer a:hover,
footer a:focus {
  color: #0b5394;
  text-decoration: underline;
}
css Contoh: pengelompokan yang sama dengan :is()
:is(nav, footer) a:is(:hover, :focus) {
  color: #0b5394;
  text-decoration: underline;
}

Selain merapikan penulisan, daftar argumen :is() bersifat memaafkan (forgiving). Apabila satu argumen tidak valid, browser mengabaikan argumen itu saja, sedangkan daftar selector biasa dengan koma membatalkan seluruh aturan bila satu selector salah. Sifat ini membuat :is() lebih tahan terhadap perbedaan dukungan antar browser.

Fungsi ini juga dapat dipakai tanpa selector dasar, misalnya :is(h1, h2) langsung di awal aturan. Bila digabungkan, bagian mana pun yang memakai :is() ikut memengaruhi bobot keseluruhan, sehingga posisinya perlu diperhatikan. Sebagai kebiasaan, tempatkan argumen berat seperti pengenal hanya bila bobotnya memang diharapkan.

:where() dan specificity nol#

Fungsi :where() memakai mesin pencocokan yang sama dengan :is(), tetapi seluruh argumennya dihitung nol. Selector :where(h1, h2, h3) tetap menjangkau ketiga jenis judul, sedangkan bobotnya tetap 0,0,0. Sifat ini menjadikan :where() tempat alami untuk gaya bawaan yang dirancang mudah ditimpa.

css Contoh: gaya default dengan :where() yang mudah ditimpa
:where(h1, h2, h3) {
  margin-block: 0 0.5em;
  font-weight: 600;
}

Setiap aturan penulisan ulang sesudahnya, betapapun sederhananya, akan mengalahkan gaya default tersebut tanpa perlu menaikkan bobot atau mengganggu urutan sumber. Komponen yang menetapkan h2 { margin-block: 0; } berbobot 0,0,1 pun otomatis menang. Pemisahan peran seperti ini mengurangi kebutuhan !important pada stylesheet yang berkembang.

Kebiasaan yang sehat adalah menulis gaya default pada bagian paling awal stylesheet dengan :where(), lalu membiarkan komponen menimpanya sesuai kebutuhan. Pendekatan ini menyelesaikan perselisihan margin dan jarak yang lazim terjadi antara gaya global dan komponen. Hasilnya, aturan komponen tetap sederhana karena tidak perlu meniru bobot gaya global.

Apabila suatu saat gaya default perlu lebih kuat, cukup pindahkan fungsinya ke :is() tanpa mengubah susunan aturan lainnya. Karena mesin pencocokannya identik, perpindahan tersebut tidak mengubah target yang terjangkau, melainkan hanya bobotnya. Perbedaan inilah yang membuat pasangan :is() dan :where() nyaman digunakan berdampingan.

:has() sebagai selector induk#

Fungsi :has() mencocokkan elemen apabila kondisi di dalam kurungnya terpenuhi, sehingga elemen induk dapat dipilih berdasarkan anaknya, hal yang selama ini memerlukan JavaScript. Kasus yang lazim adalah kartu yang memuat gambar, label yang berisi kotak centang tercentang, atau kelompok formulir yang berisi input tidak valid. Demo berikut memperagakan dua kasus pertama: kartu dengan media mendapat aksen tersendiri, dan label berubah gaya ketika kotak centangnya ditandai. Tandai kotak centang pada demo untuk melihat reaksinya.

Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Demo :has()</title>
  <link rel="stylesheet" href="index.css">
</head>
<body>
  <div class="kisi">
    <article class="kartu">
      <h2>Laporan mingguan</h2>
      <p>Rekap kegiatan dan catatan piket.</p>
    </article>
    <article class="kartu">
      <figure class="media"></figure>
      <h2>Arsip foto kegiatan</h2>
      <p>Dokumentasi acara kelas.</p>
    </article>
  </div>

  <label>
    <input type="checkbox" name="kehadiran">
    Hadir pada piket berikutnya
  </label>

<style>
/* Dasar demo */
body {
  font-family: system-ui, sans-serif;
  margin: 16px;
  color: #1f2933;
}

/* :where() sebagai gaya default berbobot nol */
:where(h2) {
  margin: 0 0 8px;
  font-size: 16px;
}

/* Kisi kartu */
.kisi {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.kartu {
  border: 1px solid #cbd5e1;
  border-radius: 8px;
  padding: 12px;
}

/* :has() menata kartu yang memuat media secara berbeda */
.kartu:has(.media) {
  border-color: #0b5394;
  background-color: #f0f6ff;
}

.media {
  height: 64px;
  border-radius: 6px;
  background-color: #9ec3e8;
  margin: 0 0 10px;
}

/* :has() bersama :checked mengubah tampilan label induknya */
label:has(:checked) {
  color: #0b5394;
  font-weight: 600;
}

label {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
}

</style>
<style>
/* Dasar demo */
body {
  font-family: system-ui, sans-serif;
  margin: 16px;
  color: #1f2933;
}

/* :where() sebagai gaya default berbobot nol */
:where(h2) {
  margin: 0 0 8px;
  font-size: 16px;
}

/* Kisi kartu */
.kisi {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.kartu {
  border: 1px solid #cbd5e1;
  border-radius: 8px;
  padding: 12px;
}

/* :has() menata kartu yang memuat media secara berbeda */
.kartu:has(.media) {
  border-color: #0b5394;
  background-color: #f0f6ff;
}

.media {
  height: 64px;
  border-radius: 6px;
  background-color: #9ec3e8;
  margin: 0 0 10px;
}

/* :has() bersama :checked mengubah tampilan label induknya */
label:has(:checked) {
  color: #0b5394;
  font-weight: 600;
}

label {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
}

</style></body>
</html>

Aturan kuncinya hanya dua. Aturan .kartu:has(.media) menjangkau kartu yang di dalamnya terdapat elemen berkelas media, sedangkan aturan label:has(:checked) menjangkau label yang berisi kontrol yang sedang tercentang. Selector di sebelah kiri tetap menjadi target, sedangkan isi fungsi :has() hanyalah syarat.

css Contoh: label yang berisi kotak centang tercentang
label:has(:checked) {
  color: #0b5394;
  font-weight: 600;
}

Pola yang sama meluas ke formulir. Elemen fieldset dapat diberi penanda berbeda bila salah satu inputnya tidak valid, sebagaimana aturan berikut, dan seluruh reaksinya terjadi hanya karena struktur markah yang sudah ada. Gaya ini membuat kesalahan pengisian terlihat tanpa skrip pemeriksa tambahan.

css Contoh: kelompok formulir yang berisi input tidak valid
fieldset:has(input:invalid) {
  border-color: #b3261e;
}

Satu hal perlu diingat ketika menulis :has(). Kondisi di dalam kurungnya dibaca sebagai hubungan relasional terhadap target, sehingga :has(> img) menuntut anak langsung, sedangkan :has(img) menerima kedalaman berapa pun. Tanda kombinator di dalam argumen tetap dipatuhi sebagaimana selector biasa.

Dukungan browser dan penerapan nyata#

Fungsi :is() dan :where() stabil pada semua browser utama sejak 2021, yaitu Chrome dan Edge versi 88, Safari 14, serta Firefox 78. Fungsi :has() menyusul lebih lambat karena dampak kinerjanya lebih besar, dan dukungannya menyatu ketika Firefox 121 dirilis pada Desember 2023. Dengan cakupan tersebut, ketiganya berstatus Baseline dan aman digunakan pada proyek baru.

Penerapan pada kisi kartu mencakup penandaan kartu bergambar, penyetelan kartu tanpa judul, dan penyesuaian lajur yang memuat konten tinggi, semuanya tanpa kelas tambahan. Fungsi :has() juga berguna pada formulir, misalnya menata kelompok yang berisi input tidak valid melalui :has(input:invalid). Kisi kartu pada demo di atas adalah bentuk paling sederhana dari pola-pola tersebut.

Sebelum ketiganya tersedia, pola-pola di atas biasanya ditulis dengan kelas yang ditambahkan JavaScript, misalnya kelas bermedia pada kartu bergambar. Selector modern memindahkan tanggung jawab itu ke CSS, sehingga markah kembali hanya memuat struktur dan makna, dan gaya tidak lagi bergantung pada skrip yang mungkin gagal dimuat. Risiko ketidaksesuaian antara kelas yang dijanjikan skrip dan kelas yang ditunggu CSS pun hilang.

Kesalahan umum#

Kesalahan pertama adalah menganggap :is() netral terhadap bobot. Pengelompokan yang tampak sederhana dapat diam-diam berbobot 1,0,0 karena satu argumen berupa pengenal, lalu gaya lain gagal menimpanya. Apabila tujuannya gaya default, pindahkan argumen yang berat ke :where() agar bobotnya kembali nol.

Kesalahan kedua adalah menyangka :has() mencocokkan isinya. Selector a:has(img) menjangkau tautan sebagai induknya, bukan gambar di dalamnya, karena target selalu berada di sebelah kiri. Apabila gambar itulah yang hendak ditata, tulis a:has(img) img atau balik pendekatannya.

Kesalahan ketiga adalah menulis kondisi :has() yang terlalu luas. Selector seperti div:has(*) memaksa browser menilai hampir seluruh pohon dokumen setiap kali struktur berubah, sehingga pembaruan gaya melambat. Beri kondisi sedetail mungkin, misalnya label:has(input:checked), agar jangkauan pemeriksaannya sempit.

Kesalahan keempat adalah mengabaikan pemeriksaan pada browser lama tanpa menilai dampaknya. Apabila :has() hanya menata aksen dekoratif, kegagalannya masih dapat diterima, sedangkan penanda status penting tetap memerlukan padanan tanpa :has(). Kelompokkan penggunaan selector modern menurut dampaknya agar kegagalan kecil tidak ikut menggagalkan fungsi utama.

Ringkasan#

  • :is() merapikan pengelompokan selector, memaafkan argumen tidak valid, dan berbobot mengikuti argumen terkuatnya.
  • :where() memakai pencocokan yang sama tetapi berbobot nol, sehingga tepat untuk gaya default yang mudah ditimpa.
  • :has() memilih elemen berdasarkan isinya, misalnya kartu bergambar dan label dengan kotak centang tercentang, tanpa JavaScript.
  • Ketiganya berstatus Baseline, dengan :is() dan :where() stabil sejak 2021 serta :has() menyatu sejak Desember 2023.

Referensi#

  • :is() — MDN — Dokumentasi fungsi :is() beserta aturan bobot argumen terkuat.
  • :has() — MDN — Dokumentasi selector relasional :has() beserta contoh dan dukungan browser.
  • Selectors — web.dev — Pelajaran selector pada Learn CSS, termasuk bagian pseudo-class dan selector relasional.