:has(): pemilih keluarga

Sejak waktu dimulai (Dalam istilah CSS), kami telah bekerja dengan aliran data dalam berbagai arti. Gaya kita akan membentuk "Lembar Gaya Santai". Pemilih kita juga bersifat menurun. Fitur ini bisa miring ke samping. Biasanya turun ke bawah. Tapi tidak pernah ke atas. Selama bertahun-tahun, kami telah mengkhayalkan "Pemilih orang tua". Dan akhirnya! Dalam bentuk pemilih pseudo :has().

Class semu CSS :has() mewakili sebuah elemen jika salah satu pemilih yang diteruskan sebagai parameter cocok dengan setidaknya satu elemen.

Namun, fungsi ini lebih dari sekadar pemilih "induk". Itu adalah cara yang baik untuk memasarkannya. Cara yang tidak begitu menarik mungkin adalah pemilih "lingkungan bersyarat". Tapi cincin itu tidak memiliki cincin yang sama. Bagaimana dengan pemilih "keluarga"?

Dukungan Browser

Sebelum melangkah lebih jauh, ada baiknya untuk menyebutkan dukungan browser. Belum sampai di sana. Tapi, semakin dekat. Belum ada dukungan Firefox, sekarang ada dalam roadmap. Namun, SDK ini sudah ada di Safari dan akan dirilis di Chromium 105. Semua demo dalam artikel ini akan memberi tahu Anda jika demo tidak didukung di browser yang digunakan.

Cara menggunakan :has

Jadi seperti apa tampilannya? Pertimbangkan HTML berikut dengan dua elemen yang seinduk dengan class everybody. Bagaimana Anda akan memilih 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 menggunakan CSS berikut.

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

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

Dalam contoh ini, elemen dengan class everybody adalah targetnya. Kondisi tersebut memiliki turunan dengan class a-good-time.

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

Namun, Anda dapat mengambil lebih dari itu karena :has() membuka banyak peluang. Bahkan mungkin belum ditemukan. Pertimbangkan beberapa di antaranya.

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 kerabat input langsung. Ke samping! css label:has(+ input) { … } Pilih article jika turunan img tidak memiliki teks alt css article:has(img:not([alt])) { … } Pilih documentElement tempat beberapa status berada dalam DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Pilih penampung tata letak yang memiliki turunan dengan jumlah ganjil css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Pilih semua item dalam petak yang tidak diarahkan ke bawah css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Pilih penampung yang berisi elemen khusus <todo-list> css main:has(todo-list) { ... } Pilih setiap css main:has(todo-list) { ... } elemen khusus dalam satu paragraf {1/3} dalam suatu paragraf yang memiliki elemen langsung/yang setara {1/3}articleahrcss p:has(+ hr) a:only-child { … }css article:has(>h1):has(>h2) { … } Pilih article dengan judul 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 { … }

Apa kasus penggunaan menarik yang dapat Anda pikirkan untuk :has()? Hal yang menarik di sini adalah yang mendorong Anda untuk merusak model mental. Hal ini membuat Anda berpikir "Bisakah saya mendekati gaya ini dengan cara yang berbeda?".

Contoh

Mari kita lihat beberapa contoh bagaimana kita dapat menggunakannya.

Kartu

Lihat demo kartu klasik. Kita dapat menampilkan informasi apa pun di kartu, misalnya: judul, subjudul, atau media tertentu. Ini 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 ketika Anda ingin memperkenalkan beberapa media? Untuk desain ini, kartu dapat dibagi menjadi dua kolom. Sebelumnya, Anda dapat membuat class baru untuk mewakili perilaku ini, misalnya card--with-media atau card--two-columns. Nama class ini tidak hanya menjadi sulit untuk dibayangkan, tetapi juga menjadi sulit dikelola dan diingat.

Dengan :has(), Anda dapat mendeteksi bahwa kartu memiliki beberapa media dan melakukan hal 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>

Anda tidak perlu meninggalkannya. Anda bisa berkreasi dengannya. Bagaimana kartu yang menampilkan konten “unggulan” dapat beradaptasi dalam tata letak? CSS ini akan membuat kartu unggulan dengan lebar penuh dari tata letak dan menempatkannya di awal {i>grid<i}.

.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-goyang 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;
}

Begitu banyak kemungkinan.

Formulir

Bagaimana dengan formulir? Mereka terkenal karena sulit untuk ditata gayanya. Salah satu contohnya adalah menata gaya input dan labelnya. Bagaimana cara memberikan sinyal bahwa suatu kolom valid, misalnya? Dengan :has(), ini menjadi jauh lebih mudah. Kita dapat mengaitkan ke pseudo-class bentuk 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);
}

Cobalah 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. Gunakan grup kolom "email" dan tambahkan pesan error ke grup tersebut.

<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 sedikit sentuhan menggugah selera 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 menyinggung hal ini dalam contoh kode. Namun, bagaimana cara menggunakan :has() dalam alur dokumen Anda? Hal ini memunculkan ide tentang bagaimana kita dapat 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 angka. Jika tidak memiliki figcaption, variabel tersebut akan mengambang dalam konten. Jika ada, figcaption akan mengisi lebar penuh dan mendapatkan margin tambahan.

Bereaksi terhadap Status

Bagaimana cara membuat gaya Anda reaktif terhadap beberapa status di markup kami. Pertimbangkan contoh dengan bilah navigasi geser "klasik". Jika Anda memiliki tombol yang dapat membuka navigasi, tombol tersebut mungkin menggunakan atribut aria-expanded. JavaScript dapat digunakan untuk memperbarui atribut yang sesuai. Jika aria-expanded adalah true, gunakan :has() untuk mendeteksi hal ini dan memperbarui gaya untuk navigasi geser. JavaScript melakukan tugasnya dan CSS dapat melakukan apa yang diinginkannya dengan informasi tersebut. Tidak perlu mengacak markup atau menambahkan nama class tambahan, dll. (Catatan: Ini bukan contoh siap produksi).

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

Dapatkah :dapat membantu menghindari kesalahan pengguna?

Apa kesamaan dari semua contoh ini? Selain menunjukkan cara menggunakan :has(), mereka tidak perlu mengubah nama class. Masing-masing menyisipkan konten baru dan memperbarui atribut. Ini adalah manfaat besar dari :has() karena dapat membantu mengurangi error pengguna. Dengan :has(), CSS dapat bertanggung jawab untuk menyesuaikan dengan modifikasi dalam DOM. Anda tidak perlu berganti-ganti nama class di JavaScript, sehingga mengurangi potensi error developer. Kita semua sudah pernah melakukannya saat salah ketik nama class dan harus berupaya menyimpannya dalam pencarian Object.

Ini adalah pemikiran yang menarik dan apakah itu mengarahkan kita ke markup yang lebih bersih dan lebih sedikit kode? JavaScript menjadi lebih sedikit karena penyesuaian JavaScript yang kami lakukan tidak sebanyak mungkin. Lebih sedikit HTML karena Anda tidak lagi memerlukan class seperti card card--has-media, dll.

Berpikir kreatif

Seperti yang disebutkan di atas, :has() mendorong Anda untuk merusak model mental. Ini adalah kesempatan untuk mencoba berbagai hal. Salah satu cara untuk mencoba melampaui batas adalah dengan membuat mekanisme game menggunakan CSS saja. Anda dapat membuat mekanisme berbasis langkah dengan formulir dan CSS, misalnya.

<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;
}

Dan hal itu membuka berbagai kemungkinan yang menarik. Anda bisa menggunakannya untuk melintasi formulir dengan transformasi. Perhatikan bahwa demo ini paling optimal dilihat di tab browser terpisah.

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

Meskipun Anda tidak akan merilisnya ke tahap produksi dalam waktu dekat, alat ini menyoroti cara Anda dapat menggunakan versi primitif. Seperti 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 pergi, apa yang tidak bisa Anda lakukan dengan :has()? Ada beberapa batasan dengan :has(). Masalah utama muncul karena hit performa.

  • Anda tidak dapat :has() dengan :has(). Namun, Anda dapat merangkai :has(). css :has(.a:has(.b)) { … }
  • Tidak ada penggunaan elemen pseudo dalam :has() css :has(::after) { … } :has(::first-letter) { … }
  • Batasi penggunaan :has() di dalam pseudos hanya menerima pemilih gabungan css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Batasi 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. Kredit kepada Byungwoo karena telah membagikan insight dan detail terkait implementasi ini.

Selesai!

Bersiaplah untuk :has(). Beri tahu teman Anda tentang hal ini dan bagikan postingan ini. Hal ini akan mengubah cara kita menyikapi CSS.

Semua demo tersedia di kumpulan CodePen ini.