Gaya Visual Berbasis Status Turunan

Fitur web: :has() :not()

Secara historis, pemilih CSS hanya dapat melintasi ke bawah—Anda dapat menata gaya turunan berdasarkan induknya, tetapi tidak dapat menata gaya induk berdasarkan turunannya. Pseudo-class :has() mengubah hal ini, sehingga Anda dapat menerapkan gaya bersyarat pada elemen penampung bergantung pada keberadaan atau status turunannya.

Dengan menggabungkan :has() dengan pseudo-class status (seperti :checked, :focus, :valid, :invalid, atau :not()), Anda dapat membuat komponen UI interaktif yang kompleks sepenuhnya di CSS, tanpa memerlukan JavaScript untuk mengalihkan class "pengubah" (seperti .is-active atau .has-error) pada elemen induk.

Hal ini sangat berguna untuk komponen yang perlu merespons interaksi internal, seperti tombol tema yang dilokalkan yang bereaksi terhadap kotak centang (:checked), grup formulir yang menandai error (:invalid), atau kartu yang ditinggikan saat link turunan difokuskan (:focus-within).

Menerapkan gaya visual penampung berbasis status

WAJIB: Anda harus menggunakan pemilih :has() pada elemen penampung untuk mendeteksi status tertentu (misalnya, :checked, :focus, :invalid) dari elemen turunan interaktifnya.

Untuk membuat komponen yang mengubah gayanya berdasarkan status turunan:

  1. Tentukan gaya visual default: Tetapkan variabel CSS pada penampung untuk menentukan status dasarnya.
  2. Terapkan penggantian berbasis status: Targetkan penampung dengan :has([child-selector]:[state]) dan tentukan ulang variabel CSS untuk status aktif atau alternatif.

Contoh: Komponen yang mengubah tema berdasarkan tombol turunan.

/* 1. Define the default state on the component container */
.theme-card {
  /* Using custom properties makes state-switching cleaner */
  --card-bg: #ffffff;
  --card-text: #333333;
  --card-border: #cccccc;

  background-color: var(--card-bg);
  color: var(--card-text);
  border: 1px solid var(--card-border);

  /* Use a transition for smooth state changes */
  transition: background-color 0.3s, color 0.3s;
}

/* 2. Apply styles when the child enters the specific state */
/* MANDATORY: Target the container and use :has() to check the descendant's state */
.theme-card:has(.theme-toggle:checked) {
  /* Override the properties for the active or alternate state */
  --card-bg: #222222;
  --card-text: #f0f0f0;
  --card-border: #555555;
}
/* You can also combine :has() and :not() to target specific negative states */
/* This selects a card that DOES NOT have a toggle in the checked state */
.theme-card:not(:has(.theme-toggle:checked)) {
  /* Optional: Explicit styles for the unchecked state if needed */
}
<!-- The container element that receives the styling -->
<div class="theme-card">
  <!-- The child element whose state controls the parent -->
  <label>
    <input type="checkbox" class="theme-toggle">
    Enable Dark Mode
  </label>

  <h2>Card Title</h2>
  <p>The style of this entire card is controlled by the checkbox above.</p>
</div>

Tips performa: Saat menggunakan :has(), cakupkan pemilih ke penampung yang paling spesifik (seperti .theme-card). Hindari menambatkannya ke elemen tingkat sangat tinggi seperti body:has(...) jika perubahan gaya dilokalkan, karena kueri :has() yang luas dapat memicu lebih banyak penghitungan ulang tata letak.

Strategi penggantian

Jika gaya visual berbasis status sangat penting untuk pengalaman pengguna atau tata letak halaman, Anda harus menyediakan penggantian untuk browser yang tidak mendukung pemilih :has(). Untuk efek dekoratif murni, :has() dapat digunakan sebagai progressive enhancement tanpa penggantian.

WAJIB: Saat menerapkan penggantian untuk fitur penting, Anda harus menggunakan @supports not selector(:has(*)) di CSS untuk menentukan penggantian berbasis class tradisional. Jika perubahan status penting bergantung pada interaksi pengguna, Anda juga harus menggunakan skrip inline kecil dengan CSS.supports() untuk mengalihkan class tersebut berdasarkan peristiwa JavaScript yang setara (misalnya, change, focus, blur) yang mewakili perubahan status.

/* Fallback CSS for older browsers */
/* We check if the browser DOES NOT support the :has() selector */
@supports not selector(:has(*)) {
  /* Define a traditional modifier class that applies the exact same overrides */
  .theme-card.is-active {
    --card-bg: #222222;
    --card-text: #f0f0f0;
    --card-border: #555555;
  }
}
/* Fallback JavaScript for older browsers */
/* Check for support before running the script to avoid unnecessary work in modern browsers */
if (!CSS.supports('selector(:has(*))')) {
  const toggle = document.querySelector('.theme-toggle');
  const card = document.querySelector('.theme-card');

  if (toggle && card) {
    // Manually toggle the fallback class when the input state changes
    toggle.addEventListener('change', (e) => {
      card.classList.toggle('is-active', e.target.checked);
    });
  }
}