:has(): sélecteur de famille

Depuis les débuts (en termes de CSS), nous avons travaillé avec une cascade sous différents sens. Nos styles composent une "feuille de style en cascade". Nos sélecteurs sont aussi appliqués en cascade. Ils peuvent se déplacer de travers. Dans la plupart des cas, elles descendent vers le bas. Mais jamais vers le haut. Depuis des années, nous imaginons un "sélecteur de parents". Et voilà ! En forme de pseudo-sélecteur :has().

La pseudo-classe CSS :has() représente un élément si l'un des sélecteurs transmis en tant que paramètres correspond à au moins un élément.

Mais il ne s'agit pas seulement d'un sélecteur "parent". C'est une bonne façon de la commercialiser. La méthode moins attrayante pourrait être le sélecteur "environnement conditionnel". Mais la sonnerie n'est pas tout à fait la même. Qu'en est-il du sélecteur "Famille" ?

Navigateurs pris en charge

Avant d'aller plus loin, il est utile de mentionner la compatibilité des navigateurs. Ce n'est pas encore tout à fait là. Mais ça se rapproche. Firefox n'est pas encore pris en charge, c'est prévu. Mais il est déjà dans Safari et devrait être publié dans Chromium 105. Toutes les démonstrations de cet article vous indiqueront si elles ne sont pas prises en charge dans le navigateur utilisé.

Utilisation de :has

À quoi cela ressemble ? Considérez le code HTML suivant, qui comporte deux éléments frères avec la classe everybody. Comment sélectionneriez-vous celui qui a un descendant avec la classe a-good-time ?

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

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

Avec :has(), vous pouvez le faire avec le CSS suivant :

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

Cela sélectionne la première instance de .everybody et applique un animation.

Dans cet exemple, l'élément avec la classe everybody est la cible. La condition a un descendant avec la classe a-good-time.

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

Toutefois, vous pouvez aller beaucoup plus loin, car :has() offre de nombreuses opportunités. Même ceux qui ne sont probablement pas encore découverts. Voici quelques exemples.

Sélectionnez les éléments figure ayant un figcaption direct. css figure:has(> figcaption) { ... } Sélectionnez des éléments anchor qui n'ont pas de descendant SVG direct css a:not(:has(> svg)) { ... } Sélectionnez des éléments label ayant un frère ou une sœur input direct. Vous tournez sur la tête ! css label:has(+ input) { … } Sélectionnez les éléments article dont le descendant img ne contient pas de texte alt css article:has(img:not([alt])) { … } Sélectionnez le documentElement dans lequel un état est présent dans le DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Sélectionnez le conteneur de mise en page avec un nombre impair d'enfants css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Sélectionnez tous les éléments d'une grille qui ne sont pas survolés css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Sélectionnez le conteneur qui contient un élément personnalisé <todo-list> css main:has(todo-list) { ... } Sélectionnez tous les a éléments individuels d'un paragraphe qui comportent un élément articlede frère ou sœur direct hrcss p:has(+ hr) a:only-child { … }css article:has(>h1):has(>h2) { … } Sélectionnez un article dont le titre est suivi d'un sous-titre. css article:has(> h1 + h2) { … } Sélectionnez :root lorsque des états interactifs sont déclenchés. css :root:has(a:hover) { … } Sélectionnez le paragraphe qui suit une figure sans figcaption css figure:not(:has(figcaption)) + p { … }

Quels cas d'utilisation intéressants pouvez-vous imaginer pour :has() ? Ce qui est fascinant ici, c'est qu'il vous incite à briser votre modèle mental. Cela vous fait réfléchir : « Pourrais-je aborder ces styles d'une manière différente ? »

Exemples

Passons en revue quelques exemples de la façon dont nous pourrions l’utiliser.

Fiches

Suivez une démo classique. Nous pouvons afficher n'importe quelle information sur notre fiche, par exemple un titre, un sous-titre ou certains contenus multimédias. Voici la fiche de base.

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

Que se passe-t-il lorsque vous voulez présenter des médias ? Pour cette conception, la fiche peut être divisée en deux colonnes. Auparavant, vous pouvez créer une classe pour représenter ce comportement, par exemple card--with-media ou card--two-columns. Ces noms de classe deviennent non seulement difficiles à imaginer, mais aussi à gérer et à retenir.

:has() vous permet de détecter que la carte contient des contenus multimédias et de réagir en conséquence. Inutile de nommer les classes de modificateurs.

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

Et vous n'avez pas besoin de le laisser là. Vous pouvez faire preuve de créativité. Comment une fiche montrant une sélection de contenus peut-elle s'adapter à une mise en page ? Ce CSS ferait une fiche mise en avant sur toute la largeur de la mise en page et la placerait au début d'une grille.

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

Que se passe-t-il si une carte mise en avant avec une bannière agite pour attirer l'attention ?

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

Les possibilités sont infinies.

Formulaires

Et les formulaires ? Ils sont connus pour être difficiles à styliser. Par exemple, vous pouvez appliquer un style aux entrées et à leurs étiquettes. Par exemple, comment signaler qu'un champ est valide ? Avec :has(), cela devient beaucoup plus facile. Nous pouvons nous associer à la forme appropriée pour les pseudo-classes, par exemple :valid et :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);
}

Essayez avec cet exemple: essayez de saisir des valeurs valides et non valides, et de déplacer la sélection.

Vous pouvez également utiliser :has() pour afficher et masquer le message d'erreur d'un champ. Prenez notre groupe de champs "Adresse e-mail" et ajoutez-y un message d'erreur.

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

Par défaut, vous masquez le message d'erreur.

.form-group__error {
  display: none;
}

Toutefois, lorsque le champ devient :invalid et qu'il n'est pas sélectionné, vous pouvez afficher le message sans avoir besoin d'autres noms de classe.

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

Aucune raison ne vous a permis d'ajouter une touche de fantaisie savoureuse lorsque vos utilisateurs interagissent avec votre formulaire. Prenez l'exemple suivant : Observez ce qui s'affiche lorsque vous saisissez une valeur valide pour la micro-interaction. La valeur :invalid entraîne l'agitation du groupe de formulaires. Mais, uniquement si l'utilisateur n'a pas de préférences de mouvement.

Contenus

Nous avons abordé ce point dans les exemples de code. Mais comment pouvez-vous utiliser :has() dans votre flux de documents ? Cela donne des idées sur la façon dont nous pouvons styliser la typographie autour des médias, par exemple.

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

Cet exemple contient des figures. Lorsqu'ils n'ont pas de figcaption, ils flottent dans le contenu. Lorsqu'un élément figcaption est présent, il occupe toute la largeur et bénéficie d'une marge supplémentaire.

Réagir à l'état

Pourquoi ne pas rendre vos styles réactifs à certains états de notre balisage ? Prenons l'exemple de la barre de navigation glissante "classique". Si vous disposez d'un bouton permettant d'activer ou de désactiver l'ouverture du menu de navigation, il peut utiliser l'attribut aria-expanded. JavaScript peut être utilisé pour mettre à jour les attributs appropriés. Lorsque aria-expanded est défini sur true, utilisez :has() pour le détecter et mettre à jour les styles de navigation glissante. JavaScript fait son travail, et CSS peut faire ce qu'il veut avec ces informations. Il n'est pas nécessaire de mélanger le balisage, d'ajouter des noms de classe supplémentaires, etc. (Remarque: il ne s'agit pas d'un exemple adapté à la production).

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

Peut-on éviter les erreurs de l'utilisateur ?

Quel est le point commun entre tous ces exemples ? Mis à part le fait qu'ils indiquent comment utiliser :has(), aucun d'entre eux n'a nécessité de modifier les noms des classes. Chacun a inséré un nouveau contenu et mis à jour un attribut. C'est un grand avantage de :has(), car il permet de limiter les erreurs de l'utilisateur. Avec :has(), le CSS a la responsabilité d'ajuster les modifications dans le DOM. Vous n'avez pas besoin de jongler avec les noms des classes en JavaScript, ce qui réduit les risques d'erreur du développeur. Nous sommes tous passés par là lorsque nous avons fait une faute de frappe dans un nom de classe et que nous devions le conserver dans les recherches Object.

Cette remarque est-elle intéressante ? Cela nous mène-t-il à un balisage plus propre et à moins de code ? Moins de JavaScript, car nous n'effectuons pas autant d'ajustements JavaScript. Moins de code HTML, car vous n'avez plus besoin de classes comme card card--has-media, etc.

Sortir des sentiers battus

Comme indiqué ci-dessus, :has() vous incite à briser le modèle mental. C’est l’occasion d’essayer différentes choses. Pour repousser les limites, vous pouvez créer des mécaniques de jeu en n'utilisant que le CSS. Vous pouvez par exemple créer un mécanisme basé sur des étapes avec des formulaires et CSS, par exemple.

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

Cela ouvre des possibilités intéressantes. pour balayer un formulaire avec des transformations. Notez qu'il est préférable de visionner cette démonstration dans un onglet distinct du navigateur.

Et pour le plaisir, que diriez-vous du grand classique des jeux de société ? Le mécanisme est plus facile à créer avec :has(). Si vous pointez sur le fil, la partie est terminée. Oui, nous pouvons créer certains de ces mécanismes de jeu avec des éléments tels que les combinateurs frères (+ et ~). Toutefois, :has() permet d'obtenir ces mêmes résultats sans avoir à utiliser des "astuces" de balisage intéressantes. Notez qu'il est préférable de visionner cette démonstration dans un onglet distinct du navigateur.

Bien que vous ne les mettiez pas en production prochainement, elles mettent en avant les différentes façons d'utiliser la primitive. Par exemple, la possibilité de chaîner un :has().

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

Performances et limites

Avant de terminer, que ne pouvez-vous pas faire avec :has() ? :has() présente des restrictions. Les principaux découlent des performances exceptionnelles enregistrées.

  • Vous ne pouvez pas :has() un :has(). Toutefois, vous pouvez associer un :has(). css :has(.a:has(.b)) { … }
  • Pas d'utilisation de pseudo-éléments dans :has() css :has(::after) { … } :has(::first-letter) { … }
  • Restriction de l'utilisation de :has() dans les pseudos n'acceptant que les sélecteurs composés css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Limiter l'utilisation de :has() après le pseudo-élément css ::part(foo):has(:focus) { … }
  • L'utilisation de :visited est toujours "false". css :has(:visited) { … }

Pour obtenir des métriques de performances réelles concernant :has(), consultez ce Glitch. Merci à Byungwoo pour avoir partagé ces informations et détails sur la mise en œuvre.

Et voilà !

Préparez-vous pour :has(). Parlez-en à vos amis et partagez ce post. Il changera la donne dans notre approche des CSS.

Toutes les démonstrations sont disponibles dans cette collection de CodePen.