:has(): pemilih keluarga

Sejak awal waktu (Dalam istilah CSS), kita telah menggunakan cascade dalam berbagai hal. Gaya kami menyusun "Cascading Style Sheet". Pemilih kami juga akan disusun secara berurutan. Mereka dapat bergerak ke samping. Dalam sebagian besar kasus, ia akan bergerak ke bawah. Namun, tidak pernah ke atas. Selama bertahun-tahun, kami membayangkan "Pemilih induk". Dan sekarang, akhirnya fitur ini hadir. Dalam bentuk pemilih pseudo :has().

Pseudo-class CSS :has() mewakili elemen jika salah satu pemilih yang diteruskan sebagai parameter cocok dengan setidaknya satu elemen.

Namun, ini lebih dari sekadar pemilih "orang tua". Itu cara yang bagus untuk memasarkannya. Cara yang tidak begitu menarik mungkin adalah pemilih "lingkungan bersyarat". Namun, hal itu tidak memiliki arti yang sama. Bagaimana dengan pemilih "keluarga"?

Dukungan Browser

Sebelum kita lanjutkan, ada baiknya untuk menyebutkan dukungan browser. Belum cukup. Namun, hal ini semakin dekat. Belum ada dukungan Firefox, tetapi ada dalam rencana pengembangan. Namun, fitur ini sudah ada di Safari dan akan dirilis di Chromium 105. Semua demo dalam artikel ini akan memberi tahu Anda jika demo tersebut tidak didukung di browser yang digunakan.

Cara menggunakan :has

Jadi seperti apa bentuknya? Pertimbangkan HTML berikut dengan dua elemen yang bersaudara dengan class everybody. Bagaimana cara Anda memilih yang memiliki turunan dengan class a-good-time?

<div class="everybody">
  <div>
    <div class="a-good-time"></div>
  </div>
</div>

<div class="everybody"></div>

Dengan :has(), Anda dapat melakukannya dengan CSS berikut.

.everybody:has(.a-good-time) {
  animation: party 21600s forwards;
}

Tindakan ini akan memilih instance .everybody pertama dan menerapkan animation.

Dalam contoh ini, elemen dengan class everybody adalah target. Kondisinya adalah memiliki turunan dengan class a-good-time.

<target>:has(<condition>) { <styles> }

Namun, Anda dapat melakukannya lebih jauh karena :has() membuka banyak peluang. Bahkan yang mungkin belum ditemukan. Pertimbangkan beberapa hal berikut.

Pilih elemen figure yang memiliki figcaption langsung. css figure:has(> figcaption) { ... } Pilih anchor yang tidak memiliki turunan SVG langsung css a:not(:has(> svg)) { ... } Pilih label yang memiliki saudara input langsung. Kita ke samping. css label:has(+ input) { … } Pilih article jika turunan img tidak memiliki teks alt css article:has(img:not([alt])) { … } Pilih documentElement jika beberapa status ada di DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Pilih penampung tata letak dengan jumlah turunan ganjil css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Pilih semua item dalam petak yang tidak diarahkan kursor mouse css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Pilih penampung yang berisi elemen kustom <todo-list> css main:has(todo-list) { ... } Pilih setiap a tunggal dalam paragraf yang memiliki elemen hr saudara langsung css p:has(+ hr) a:only-child { … } Pilih article jika beberapa kondisi terpenuhi css article:has(>h1):has(>h2) { … } Campur semuanya. Pilih article dengan judul yang diikuti subtitel css article:has(> h1 + h2) { … } Pilih :root saat status interaktif dipicu css :root:has(a:hover) { … } Pilih paragraf yang mengikuti figure yang tidak memiliki figcaption css figure:not(:has(figcaption)) + p { … }

Kasus penggunaan menarik apa yang dapat Anda pikirkan untuk :has()? Hal yang menarik di sini adalah hal ini mendorong Anda untuk mematahkan model mental Anda. Hal ini membuat Anda berpikir, "Apakah saya dapat mendekati gaya ini dengan cara yang berbeda?".

Contoh

Mari kita bahas beberapa contoh cara menggunakannya.

Kartu

Lihat demo kartu klasik. Kita dapat menampilkan informasi apa pun di kartu, misalnya: judul, subtitel, atau beberapa media. Berikut adalah kartu dasarnya.

<li class="card">
  <h2 class="card__title">
      <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
</li>

Apa yang terjadi jika Anda ingin memperkenalkan beberapa media? Untuk desain ini, kartu dapat dibagi menjadi dua kolom. Sebelumnya, Anda mungkin membuat class baru untuk mewakili perilaku ini, misalnya card--with-media atau card--two-columns. Nama class ini tidak hanya sulit dibuat, tetapi juga sulit dikelola dan diingat.

Dengan :has(), Anda dapat mendeteksi bahwa kartu memiliki beberapa media dan melakukan tindakan yang sesuai. Tidak perlu nama class pengubah.

<li class="card">
  <h2 class="card__title">
    <a href="/article.html">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
</li>

Dan Anda tidak perlu membiarkannya di sana. Anda dapat berkreasi dengan fitur ini. Bagaimana kartu yang menampilkan konten “unggulan” dapat beradaptasi dalam tata letak? CSS ini akan membuat kartu unggulan memiliki lebar penuh tata letak dan menempatkannya di awal petak.

.card:has(.card__banner) {
  grid-row: 1;
  grid-column: 1 / -1;
  max-inline-size: 100%;
  grid-template-columns: 1fr 1fr;
  border-left-width: var(--size-4);
}

Bagaimana jika kartu unggulan dengan banner bergoyang untuk menarik perhatian?

<li class="card">
  <h2 class="card__title">
    <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
  <div class="card__banner"></div>
</li>

.card:has(.card__banner) {
  --color: var(--green-3-hsl);
  animation: wiggle 6s infinite;
}

Ada begitu banyak kemungkinan.

Formulir

Bagaimana dengan formulir? Gaya rambut ini dikenal sulit diatur. Salah satu contohnya adalah menata gaya input dan labelnya. Bagaimana cara memberi sinyal bahwa kolom valid, misalnya? Dengan :has(), hal ini menjadi jauh lebih mudah. Kita dapat menghubungkan ke pseudo-class formulir yang relevan, misalnya :valid dan :invalid.

<div class="form-group">
  <label for="email" class="form-label">Email</label>
  <input
    required
    type="email"
    id="email"
    class="form-input"
    title="Enter valid email address"
    placeholder="Enter valid email address"
  />   
</div>
label {
  color: var(--color);
}
input {
  border: 4px solid var(--color);
}

.form-group:has(:invalid) {
  --color: var(--invalid);
}

.form-group:has(:focus) {
  --color: var(--focus);
}

.form-group:has(:valid) {
  --color: var(--valid);
}

.form-group:has(:placeholder-shown) {
  --color: var(--blur);
}

Coba dalam contoh ini: Coba masukkan nilai yang valid dan tidak valid, lalu aktifkan dan nonaktifkan fokus.

Anda juga dapat menggunakan :has() untuk menampilkan dan menyembunyikan pesan error untuk kolom. Ambil grup kolom “email” dan tambahkan pesan error ke dalamnya.

<div class="form-group">
  <label for="email" class="form-label">
    Email
  </label>
  <div class="form-group__input">
    <input
      required
      type="email"
      id="email"
      class="form-input"
      title="Enter valid email address"
      placeholder="Enter valid email address"
    />   
    <div class="form-group__error">Enter a valid email address</div>
  </div>
</div>

Secara default, Anda menyembunyikan pesan error.

.form-group__error {
  display: none;
}

Namun, saat kolom menjadi :invalid dan tidak difokuskan, Anda dapat menampilkan pesan tanpa memerlukan nama class tambahan.

.form-group:has(:invalid:not(:focus)) .form-group__error {
  display: block;
}

Tidak ada alasan Anda tidak dapat menambahkan sentuhan imajinasi yang menarik saat pengguna berinteraksi dengan formulir Anda. Pertimbangkan contoh ini. Tonton saat Anda memasukkan nilai yang valid untuk interaksi mikro. Nilai :invalid akan menyebabkan grup formulir bergetar. Namun, hanya jika pengguna tidak memiliki preferensi gerakan.

Konten

Kita telah membahasnya dalam contoh kode. Namun, bagaimana Anda dapat menggunakan :has() dalam alur dokumen? Misalnya, teknik ini akan memunculkan ide tentang cara kita menata gaya tipografi di sekitar media.

figure:not(:has(figcaption)) {
  float: left;
  margin: var(--size-fluid-2) var(--size-fluid-2) var(--size-fluid-2) 0;
}

figure:has(figcaption) {
  width: 100%;
  margin: var(--size-fluid-4) 0;
}

figure:has(figcaption) img {
  width: 100%;
}

Contoh ini berisi gambar. Jika tidak memiliki figcaption, elemen akan mengambang dalam konten. Jika ada figcaption, figcaption akan menempati lebar penuh dan mendapatkan margin tambahan.

Merespons Status

Bagaimana jika gaya Anda bereaksi terhadap beberapa status dalam markup kita. Pertimbangkan contoh dengan menu navigasi geser "klasik". Jika Anda memiliki tombol yang mengalihkan pembukaan navigasi, tombol tersebut dapat menggunakan atribut aria-expanded. JavaScript dapat digunakan untuk memperbarui atribut yang sesuai. Jika aria-expanded adalah true, gunakan :has() untuk mendeteksinya dan memperbarui gaya untuk navigasi geser. JavaScript melakukan tugasnya dan CSS dapat melakukan apa yang diinginkan dengan informasi tersebut. Tidak perlu mengacak markup atau menambahkan nama class tambahan, dll. (Catatan: Ini bukan contoh yang siap produksi).

:root:has([aria-expanded="true"]) {
    --open: 1;
}
body {
    transform: translateX(calc(var(--open, 0) * -200px));
}

Dapatkah :has membantu menghindari error pengguna?

Apa kesamaan dari semua contoh ini? Selain menunjukkan cara menggunakan :has(), tidak satu pun dari contoh tersebut yang memerlukan perubahan nama class. Setiap perubahan tersebut menyisipkan konten baru dan memperbarui atribut. Ini adalah manfaat besar dari :has(), karena dapat membantu memitigasi error pengguna. Dengan :has(), CSS dapat mengambil tanggung jawab untuk menyesuaikan dengan modifikasi di DOM. Anda tidak perlu mengelola nama class di JavaScript, sehingga mengurangi potensi error developer. Kita semua pernah mengalaminya saat salah ketik nama class dan harus menyimpannya dalam pencarian Object.

Ini adalah pemikiran yang menarik dan apakah hal ini akan mengarahkan kita ke markup yang lebih bersih dan lebih sedikit kode? Lebih sedikit JavaScript karena kita tidak melakukan banyak penyesuaian JavaScript. HTML yang lebih sedikit karena Anda tidak lagi memerlukan class seperti card card--has-media, dll.

Berpikir di luar kebiasaan

Seperti yang disebutkan di atas, :has() mendorong Anda untuk mematahkan model mental. Ini adalah kesempatan untuk mencoba hal-hal yang berbeda. Salah satu cara untuk mencoba dan mendorong batas adalah dengan membuat mekanisme game hanya dengan CSS. Misalnya, Anda dapat membuat mekanisme berbasis langkah dengan formulir dan CSS.

<div class="step">
  <label for="step--1">1</label>
  <input id="step--1" type="checkbox" />
</div>
<div class="step">
  <label for="step--2">2</label>
  <input id="step--2" type="checkbox" />
</div>
.step:has(:checked), .step:first-of-type:has(:checked) {
  --hue: 10;
  opacity: 0.2;
}


.step:has(:checked) + .step:not(.step:has(:checked)) {
  --hue: 210;
  opacity: 1;
}

Hal ini membuka kemungkinan yang menarik. Anda dapat menggunakannya untuk menjelajahi formulir dengan transformasi. Perhatikan bahwa demo ini paling baik dilihat di tab browser terpisah.

Dan untuk bersenang-senang, bagaimana dengan game buzz wire klasik? Mekanisme ini lebih mudah dibuat dengan :has(). Jika kabel diarahkan, game akan berakhir. Ya, kita dapat membuat beberapa mekanisme game ini dengan hal-hal seperti kombinator saudara (+ dan ~). Namun, :has() adalah cara untuk mencapai hasil yang sama tanpa harus menggunakan "trik" markup yang menarik. Perhatikan bahwa demo ini paling baik dilihat di tab browser terpisah.

Meskipun Anda tidak akan segera memasukkannya ke dalam produksi, kode ini menyoroti cara Anda dapat menggunakan primitif. Misalnya, dapat membuat rantai :has().

:root:has(#start:checked):has(.game__success:hover, .screen--win:hover)
.screen--win {
  --display-win: 1;
}

Performa dan batasan

Sebelum kita lanjut, apa yang tidak dapat Anda lakukan dengan :has()? Ada beberapa batasan terkait :has(). Penyebab utamanya adalah penurunan performa.

  • Anda tidak dapat :has() :has(). Namun, Anda dapat membuat rantai :has(). css :has(.a:has(.b)) { … }
  • Tidak ada penggunaan elemen pseudo dalam :has() css :has(::after) { … } :has(::first-letter) { … }
  • Membatasi penggunaan :has() di dalam pseudo yang hanya menerima pemilih gabungan css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Membatasi penggunaan :has() setelah elemen pseudo css ::part(foo):has(:focus) { … }
  • Penggunaan :visited akan selalu salah css :has(:visited) { … }

Untuk metrik performa sebenarnya yang terkait dengan :has(), lihat Glitch ini. Terima kasih kepada Byungwoo yang telah membagikan insight dan detail tentang penerapan ini.

Selesai!

Bersiaplah untuk :has(). Beri tahu teman Anda dan bagikan postingan ini. Hal ini akan menjadi game changer untuk cara kita mendekati CSS.

Semua demo tersedia di koleksi CodePen ini.