Style basé sur l'état de l'enfant

Fonctionnalités Web : :has() :not()

Auparavant, les sélecteurs CSS ne pouvaient parcourir que vers le bas : vous pouviez appliquer un style à un enfant en fonction de son parent, mais pas à un parent en fonction de son enfant. La pseudo-classe :has() modifie ce comportement, ce qui vous permet de styliser un élément conteneur de manière conditionnelle en fonction de la présence ou de l'état de ses descendants.

En combinant :has() avec des pseudo-classes d'état (comme :checked, :focus, :valid, :invalid ou :not()), vous pouvez créer des composants d'interface utilisateur complexes et interactifs entièrement en CSS, sans avoir besoin de JavaScript pour activer ou désactiver les classes "modificateur" (comme .is-active ou .has-error) sur les éléments parents.

Cela est particulièrement utile pour les composants qui doivent répondre à des interactions internes, comme un bouton d'activation/désactivation de thème localisé réagissant à une case à cocher (:checked), un groupe de formulaires mettant en évidence une erreur (:invalid) ou une fiche s'élevant lorsqu'un lien enfant est sélectionné (:focus-within).

Implémenter un style de conteneur basé sur l'état

OBLIGATOIRE : Vous devez utiliser le sélecteur :has() sur l'élément conteneur pour détecter l'état spécifique (par exemple, :checked, :focus, :invalid) de son élément enfant interactif.

Pour créer un composant qui modifie son style en fonction de l'état d'un enfant :

  1. Définir le style par défaut : définissez des variables CSS sur le conteneur pour définir son état de base.
  2. Appliquer des remplacements basés sur l'état : ciblez le conteneur avec :has([child-selector]:[state]) et redéfinissez les variables CSS pour l'état actif ou alternatif.

Exemple : Un composant qui change de thème en fonction d'un bouton d'activation/de désactivation enfant.

/* 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>

Conseil d'optimisation des performances : Lorsque vous utilisez :has(), limitez le sélecteur au conteneur le plus spécifique possible (comme .theme-card). Évitez de l'ancrer à des éléments de très haut niveau comme body:has(...) si les modifications de style sont localisées, car les requêtes :has() larges peuvent déclencher davantage de recalculs de mise en page.

Stratégies de remplacement

Si le style basé sur l'état est essentiel pour l'expérience utilisateur ou la mise en page, vous devez fournir une solution de remplacement pour les navigateurs qui ne sont pas compatibles avec le sélecteur :has(). Pour les effets purement décoratifs, :has() peut être utilisé comme amélioration progressive sans solution de remplacement.

OBLIGATOIRE : Lorsque vous implémentez une solution de remplacement pour les fonctionnalités critiques, vous devez utiliser @supports not selector(:has(*)) dans votre CSS pour définir une solution de remplacement traditionnelle basée sur les classes. Si le changement d'état critique dépend de l'interaction de l'utilisateur, vous devez également utiliser un petit script intégré avec CSS.supports() pour activer ou désactiver cette classe en fonction de l'événement JavaScript équivalent (par exemple, change, focus, blur) représentant le changement d'état.

/* 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);
    });
  }
}