:has(): pemilih keluarga

Sejak dimulai (Dalam istilah CSS), kita telah bekerja dengan jenjang dalam berbagai arti. Gaya kita menyusun "Cascading Style Sheet". Dan pemilih kita juga mengalir. Tombol bisa miring. Umumnya, turun ke bawah. Tapi tidak pernah ke atas. Selama bertahun-tahun, kita mengkhayalkan "Pemilih orang tua". Dan sekarang 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, peran ini lebih dari sekadar "induk" pemilih. Itu cara yang bagus untuk memasarkannya. Cara yang tidak begitu menarik mungkin adalah "lingkungan bersyarat" pemilih. Tapi cincinnya tidak sama persis. Bagaimana dengan "keluarga" pemilih?

Dukungan Browser

Sebelum melangkah lebih jauh, ada baiknya Anda mengetahui dukungan browser. Belum selesai. Tapi, semakin dekat. Belum ada dukungan Firefox. Dukungan ini sudah ada dalam roadmap. Namun, aplikasi 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 setara dengan class everybody. Bagaimana Anda akan memilih class 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 pertama .everybody dan menerapkan animation.

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

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

Namun, Anda dapat melakukannya lebih jauh 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 seinduk langsung input. Pergi ke samping! css label:has(+ input) { … } Pilih article jika img turunan tidak memiliki teks alt css article:has(img:not([alt])) { … } Pilih documentElement yang memiliki beberapa status 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 ditunjuk dengan kursor 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 solo a dalam paragraf yang memiliki elemen saudara langsung hr css p:has(+ hr) a:only-child { … } Pilih article yang beberapa kondisi terpenuhi css article:has(>h1):has(>h2) { … } Padukan. Pilih article yang judulnya diikuti dengan subtitel css article:has(> h1 + h2) { … } Pilih :root saat status interaktif dipicu css :root:has(a:hover) { … } Pilih paragraf setelah 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 kesempatan mendorong Anda untuk mematahkan model mental Anda. Hal ini membuat Anda berpikir "Bisakah saya mendekati gaya ini dengan cara yang berbeda?".

Contoh

Mari kita lihat beberapa contoh bagaimana kita bisa menggunakannya.

Kartu

Ikuti demo kartu klasik. Kita dapat menampilkan informasi apa pun di kartu, misalnya: judul, subjudul, atau media. Berikut 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 sulit untuk dibuat, 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 juga tidak perlu meninggalkannya. Anda bisa menggunakannya untuk berpikir kreatif. Bagaimana kartu yang menampilkan konten “unggulan” dapat beradaptasi dalam tata letak? CSS ini akan membuat kartu unggulan selebar 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 bergerak 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 sulit untuk ditata gayanya. Salah satu contoh hal ini adalah penataan gaya input dan labelnya. Misalnya, bagaimana cara kami menunjukkan bahwa suatu kolom valid? Dengan :has(), proses ini menjadi jauh lebih mudah. Kita dapat menghubungkan ke class semu 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);
}

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. Ambil 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 perlu nama class tambahan.

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

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

Konten

Kita telah membahasnya dalam contoh kode. Namun, bagaimana Anda dapat menggunakan :has() dalam alur dokumen Anda? Hal tersebut memunculkan ide tentang bagaimana kita bisa menata gaya tipografi di sekitar media, misalnya.

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, elemen ini akan mengambang di dalam konten. Jika ada, figcaption akan menempati lebar penuh dan mendapatkan margin tambahan.

Bereaksi terhadap Status

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

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

Dapatkah :has membantu menghindari kesalahan pengguna?

Apa kesamaan dari semua contoh-contoh ini? Selain menunjukkan cara menggunakan :has(), keduanya tidak memerlukan modifikasi nama class. Mereka masing-masing menyisipkan konten baru dan memperbarui atribut. Hal ini adalah manfaat besar :has(), karena dapat membantu mengurangi error pengguna. Dengan :has(), CSS dapat bertanggung jawab untuk menyesuaikan modifikasi di DOM. Anda tidak perlu membolak-balik nama class di JavaScript, sehingga mengurangi potensi error developer. Kita semua pernah berada di sana saat salah mengetik nama class dan harus menyimpannya dalam pencarian Object.

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

Berpikir kreatif

Seperti disebutkan di atas, :has() mendorong Anda untuk merusak model mental. Ini adalah kesempatan untuk mencoba berbagai hal. Salah satu cara untuk mencoba dan menembus batas adalah dengan membuat mekanisme game hanya dengan CSS. Anda dapat membuat mekanika 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 kemungkinan yang menarik. Anda bisa menggunakannya untuk menjelajahi formulir dengan transformasi. Perhatikan bahwa demo ini paling optimal dilihat di tab browser terpisah.

Dan untuk bersenang-senang, bagaimana dengan permainan dengkuran klasik? Mekanik lebih mudah dibuat dengan :has(). Jika kabel diarahkan ke atas, game selesai. Ya, kita dapat membuat beberapa mekanika game ini dengan fitur seperti kombinator seinduk (+ 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, hal ini menyoroti cara menggunakan versi 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 pergi, apa yang tidak dapat Anda lakukan dengan :has()? Ada beberapa batasan dengan :has(). Penyebab utama muncul karena hit 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) { … }
  • Batasi penggunaan :has() di dalam pseudo 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. Terima kasih kepada Byungwoo yang telah membagikan insight dan detail terkait implementasi tersebut.

Selesai!

Bersiaplah untuk :has(). Beri tahu teman Anda tentang hal itu dan bagikan postingan ini, akan membawa perubahan besar dalam pendekatan kami terhadap CSS.

Semua demo tersedia di Pengumpulan CodePen ini.