:has(): sélecteur de famille

Depuis le début (en termes CSS), nous avons travaillé avec une cascade dans différents sens. Nos styles composent une "feuille de style en cascade". Et nos sélecteurs sont également en cascade. Ils peuvent se tromper. Dans la plupart des cas, ils vont vers le bas. Mais jamais vers le haut. Depuis des années, nous rêverons d'un "sélecteur de parent". Et c'est enfin arrivé ! Sous la forme d'un 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, c'est plus qu'un « parent » sélecteur. C'est une bonne façon de le commercialiser. La méthode moins attrayante pourrait être "l'environnement conditionnel" sélecteur. Mais il n'a pas tout à fait le même sonnerie. Que pensez-vous de la « famille » sélecteur ?

Navigateurs pris en charge

Avant d'aller plus loin, il convient de mentionner la compatibilité des navigateurs. Ce n'est pas encore tout à fait fini. Mais ça s'approche. Firefox n'est pas encore compatible avec cette application, nous y travaillons. Mais elle est déjà dans Safari et devrait sortir dans Chromium 105. Toutes les démonstrations de cet article vous indiqueront si elles ne sont pas compatibles avec le navigateur utilisé.

Utilisation de :has

À quoi cela ressemble ? Prenons l'exemple du code HTML suivant, qui comporte deux éléments frères avec la classe everybody. Comment sélectionneriez-vous celle 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;
}

Cette opération 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 est d'avoir un descendant avec la classe a-good-time.

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

Mais 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 les éléments anchor qui n'ont pas de descendant SVG direct css a:not(:has(> svg)) { ... } Sélectionnez les label ayant un frère input direct. On avance de travers ! css label:has(+ input) { … } Sélectionnez les articles où un img descendant ne comporte pas de texte alt css article:has(img:not([alt])) { … } Sélectionnez le documentElement où un état est présent dans le DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Sélectionner 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électionner tous les éléments d'une grille qui n'ont pas été pointé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électionner chaque solo a d'un paragraphe qui a un élément frère direct hr css p:has(+ hr) a:only-child { … }. Sélectionnez un article où plusieurs conditions sont remplies css article:has(>h1):has(>h2) { … } Mélangez ça. Sélectionnez une article où un 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 qui n'a pas de figcaption css figure:not(:has(figcaption)) + p { … }

Quels cas d'utilisation intéressants de :has() peuvent vous donner ? Ce qui est fascinant ici, c'est que cela vous encourage à briser votre modèle mental. Cela vous fait réfléchir : « Pourrais-je aborder ces styles d'une autre manière ? ».

Exemples

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

Fiches

Suivre une démo de la carte classique Nous pourrions afficher n'importe quelle information dans notre fiche, par exemple un titre, un sous-titre ou un média. 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 carte peut être divisée en deux colonnes. Avant, vous pouviez 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 à inventer, mais aussi à tenir et à retenir.

Avec :has(), vous pouvez détecter que la carte contient du contenu multimédia et prendre les mesures appropriées. Pas besoin de noms de classe de modificateur.

<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 présentant du contenu mis en avant peut-elle s'adapter à une mise en page ? Avec ce CSS, une carte mise en avant occupe toute la largeur de la mise en page et la place 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 sélectionnée 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;
}

Tant de possibilités.

Formulaires

Qu'en est-il des 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 est beaucoup plus facile. Nous pouvons associer des raccordements aux pseudo-classes de formulaire pertinentes, 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);
}

Faites un essai avec cet exemple: essayez de saisir des valeurs valides et non valides, et mettez en avant et désactivez la sélection.

Vous pouvez également utiliser :has() pour afficher et masquer le message d'erreur d'un champ. Prenez notre groupe de champs "email" 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, le message d'erreur est masqué.

.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 de noms de classe supplémentaires.

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

Vous ne pourriez pas y ajouter une touche de fantaisie quand vos utilisateurs interagissent avec votre formulaire. Prenez l'exemple suivant : Soyez attentif lorsque vous saisissez une valeur valide pour la micro-interaction. Si la valeur :invalid est définie, le groupe de formulaires secoue. Mais seulement si l'utilisateur n'a aucune préférence de mouvement.

Contenu

Nous avons abordé ce sujet dans les exemples de code. Mais comment utiliser :has() dans votre flux de documents ? Cela propose des idées sur la façon dont nous pourrions 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. S'ils n'ont pas de figcaption, ils flottent dans le contenu. Lorsqu'un figcaption est présent, ils occupent toute la largeur et bénéficient d'une marge supplémentaire.

Réagir à l'état

Que diriez-vous de rendre vos styles réactifs à un état donné de notre balisage ? Prenons l'exemple de la version classique barre de navigation coulissante. Si vous disposez d'un bouton qui permet ou non d'ouvrir le 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 du menu de navigation glissant. JavaScript fait son travail et les CSS peuvent faire ce qu'il veut avec ces informations. Il n'est pas nécessaire de mélanger le balisage ni d'ajouter des noms de classe supplémentaires, etc. (Remarque: il ne s'agit pas d'un exemple prêt pour la production.)

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

Peut-il aider à éviter les erreurs de l'utilisateur ?

Quel est le point commun entre tous ces exemples ? Mis à part le fait qu'elles expliquent comment utiliser :has(), aucune d'entre elles n'a nécessité de modifier le nom des classes. Chacun d'eux a inséré un nouveau contenu et mis à jour un attribut. C'est l'un des grands avantages de :has(), car il permet de limiter les erreurs de l'utilisateur. Avec :has(), le CSS peut se charger de s'adapter aux modifications du DOM. Vous n'avez pas besoin de jongler entre les noms de classes en JavaScript, ce qui réduit les risques d'erreur des développeurs. Nous sommes tous passés par là lorsque nous avons saisi un nom de classe et avons dû les conserver dans les recherches Object.

Cette pensée est intéressante et nous mène-t-elle à un balisage plus clair 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 telles que card card--has-media, etc.

Sortir des sentiers battus

Comme indiqué ci-dessus, :has() vous encourage à briser le modèle mental. C'est l'occasion d'essayer des choses différentes. L'un des moyens d'essayer de repousser les limites est de créer les mécanismes de jeu uniquement avec le CSS. Vous pouvez par exemple créer un mécanisme basé sur les pas avec des formulaires et CSS.

<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. Vous pouvez l'utiliser pour balayer un formulaire avec des transformations. Notez qu'il est préférable d'afficher cette démonstration dans un onglet de navigateur distinct.

Et pour le plaisir, que diriez-vous du jeu classique Buzz WiFi ? Le mécanisme est plus facile à créer avec :has(). Si on passe la souris 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 les mêmes résultats sans avoir à utiliser d'astuces intéressantes en matière de balisage. Notez qu'il est préférable d'afficher cette démonstration dans un onglet de navigateur distinct.

Bien que vous ne les déploierez pas bientôt en production, ils mettent en évidence des façons d'utiliser les primitives. Par exemple, la possibilité d'enchaîner un :has().

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

Performances et limites

Avant de nous quitter, que ne pouvez-vous pas faire avec :has() ? Des restrictions s'appliquent à :has(). Les principaux sont dus aux performances en termes de performances.

  • Vous ne pouvez pas :has() un :has(). Toutefois, vous pouvez enchaîner des :has(). css :has(.a:has(.b)) { … }
  • Aucune utilisation de pseudo-élément dans :has() css :has(::after) { … } :has(::first-letter) { … }
  • Restreindre 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 sera toujours "false" css :has(:visited) { … }

Pour connaître les métriques de performances réelles liées à :has(), consultez ce Glitch. Merci à Byungwoo d'avoir partagé ces insights et détails sur l'implémentation.

Et voilà !

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

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