:has(): sélecteur de famille

Depuis le début des temps (en termes de CSS), nous avons travaillé avec une cascade dans différents sens. Nos styles composent une "feuille de style en cascade". Nos sélecteurs sont également en cascade. Ils peuvent aller de gauche à droite. Dans la plupart des cas, elles sont orientées vers le bas. Mais jamais vers le haut. Depuis des années, nous rêvions d'un "sélecteur de parents". Et c'est enfin le cas ! 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 il ne s'agit pas seulement d'un sélecteur "parent". C'est une belle façon de le commercialiser. Le sélecteur "Environnement conditionnel" peut être moins intéressant. Mais ça ne sonne pas tout à fait pareil. Qu'en est-il du sélecteur "famille" ?

Navigateurs pris en charge

Avant d'aller plus loin, il est important de mentionner la compatibilité avec les navigateurs. Ce n'est pas tout à fait ça. Mais on s'en rapproche. Firefox n'est pas encore compatible, mais il le sera prochainement. Il est déjà disponible dans Safari et devrait être publié dans Chromium 105. Toutes les démonstrations de cet article vous indiqueront si elles ne sont pas compatibles avec le navigateur utilisé.

Utiliser :has

À quoi cela ressemble ? Prenons l'exemple HTML suivant, qui comporte deux éléments frères de la classe everybody. Comment sélectionner celui qui possède un descendant de 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 code 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 de la classe everybody est la cible. La condition consiste à avoir un descendant de la classe a-good-time.

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

Mais vous pouvez aller beaucoup plus loin, car :has() offre de nombreuses possibilités. et même celles qui ne l'ont pas encore été. Voici quelques exemples.

Sélectionnez les éléments figure qui disposent d'un figcaption direct. css figure:has(> figcaption) { ... } Sélectionnez les anchor qui n'ont pas de descendant SVG direct. css a:not(:has(> svg)) { ... } Sélectionnez les label qui ont un frère input direct. Je vais de côté ! css label:has(+ input) { … } Sélectionnez les article où un descendant img 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é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 en survol css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Sélectionnez le conteneur contenant un élément personnalisé <todo-list> css main:has(todo-list) { ... } Sélectionnez chaque a individuel dans un paragraphe qui comporte un élément hr frère direct css p:has(+ hr) a:only-child { … } Sélectionnez un article où plusieurs conditions sont remplies css article:has(>h1):has(>h2) { … } Mélangez-les. Sélectionnez un article où un titre est suivi d'un sous-titre css article:has(> h1 + h2) { … } Sélectionnez le :root lorsque des états interactifs sont déclenchés css :root:has(a:hover) { … } Sélectionnez le paragraphe qui suit un figure qui ne comporte pas de figcaption css figure:not(:has(figcaption)) + p { … }

Quels cas d'utilisation intéressants pouvez-vous imaginer pour :has() ? Ce qui est fascinant, c'est que cela vous encourage à briser votre modèle mental. Vous vous demandez alors si vous pourriez aborder ces styles différemment.

Exemples

Voyons comment l'utiliser.

Fiches

Regardez une démonstration de la carte classique. Nous pouvons afficher n'importe quelle information dans notre fiche, par exemple un titre, un sous-titre ou un contenu multimé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 si vous souhaitez présenter des contenus multimédias ? Pour cette conception, la fiche peut être divisée en deux colonnes. Auparavant, 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 à imaginer, mais aussi à entretenir et à retenir.

Avec :has(), vous pouvez détecter que la fiche contient des contenus multimédias et prendre les mesures appropriées. Il n'est pas nécessaire de modifier les noms de classe.

<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 "sélectionné" peut-elle s'adapter dans une mise en page ? Ce CSS ferait en sorte qu'une fiche sélectionnée occupe 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 fiche sélectionnée avec une bannière se balance 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 nombreuses.

Formulaires

Qu'en est-il des formulaires ? Elles sont réputées difficiles à coiffer. Par exemple, vous pouvez styliser les entrées et leurs libellés. Comment signaler qu'un champ est valide, par exemple ? Avec :has(), cela devient beaucoup plus facile. Nous pouvons nous connecter 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);
}

Essayez-le dans cet exemple: essayez de saisir des valeurs valides et non valides, et activez 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 "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 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 pouvez ajouter une touche de fantaisie à votre formulaire. Prenez l'exemple suivant : Regardez ce qui se passe lorsque vous saisissez une valeur valide pour la micro-interaction. Une valeur :invalid entraîne le tremblement du groupe de formulaires. Mais uniquement si l'utilisateur n'a pas de préférences de mouvement.

Contenu

Nous en avons parlé dans les exemples de code. Mais comment pouvez-vous utiliser :has() dans votre flux de documents ? Il nous donne des idées sur la façon dont nous pourrions styliser la typographie autour des contenus multimé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, il occupe toute la largeur et bénéficie d'une marge supplémentaire.

Réagir à l'état

Que diriez-vous de rendre vos styles réactifs à un état dans notre balisage ? Prenons l'exemple de la barre de navigation coulissante "classique". Si vous avez un bouton qui active/désactive l'ouverture de la navigation, il peut utiliser l'attribut aria-expanded. JavaScript peut être utilisé pour mettre à jour les attributs appropriés. Lorsque aria-expanded est true, utilisez :has() pour le détecter et mettre à jour les styles de la barre de navigation coulissante. JavaScript fait son travail, et le CSS peut faire ce qu'il veut avec ces informations. Vous n'avez pas besoin de réorganiser le balisage ni d'ajouter des noms de classe supplémentaires, etc. (Remarque: Il ne s'agit pas d'un exemple prêt à être mis en production).

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

La valeur :has peut-elle aider à éviter les erreurs de la part des utilisateurs ?

Qu'ont en commun tous ces exemples ? En dehors du fait qu'ils montrent comment utiliser :has(), aucun d'eux n'a nécessité de modifier les noms de classe. Ils ont chacun inséré du nouveau contenu et mis à jour un attribut. C'est un avantage majeur de :has(), car il peut aider à atténuer les erreurs de l'utilisateur. Avec :has(), le CSS peut s'adapter aux modifications apportées au DOM. Vous n'avez pas besoin de jongler avec les noms de classe en JavaScript, ce qui réduit le risque d'erreurs de développeur. Nous avons tous déjà fait une faute de frappe dans le nom d'une classe et avons dû le conserver dans des recherches Object.

C'est une idée intéressante. Cela nous conduit-il à un balisage plus propre et à moins de code ? Moins de code 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.

Penser autrement

Comme indiqué ci-dessus, :has() vous encourage à briser le modèle mental. C'est l'occasion d'essayer différentes choses. Pour essayer de repousser les limites, vous pouvez créer des mécanismes de jeu avec le CSS uniquement. Vous pouvez créer une mécanique basée sur des étapes avec des formulaires et du 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. Vous pouvez l'utiliser pour parcourir un formulaire avec des transformations. Remarque : Il est préférable de regarder cette démonstration dans un nouvel onglet de votre navigateur.

Et pour s'amuser, pourquoi ne pas jouer au jeu classique du fil électrique ? La mécanique est plus facile à créer avec :has(). Si vous pointez sur le fil, la partie est terminée. Oui, nous pouvons créer certaines de ces mécaniques de jeu à l'aide d'éléments tels que les combinateurs frères (+ et ~). Toutefois, :has() permet d'obtenir les mêmes résultats sans avoir à utiliser des "astuces" de balisage intéressantes. Remarque : Il est préférable de regarder cette démonstration dans un nouvel onglet de votre navigateur.

Bien que vous ne les utilisiez pas en production de sitôt, ils mettent en évidence les différentes façons dont vous pouvez utiliser la primitive. Par exemple, vous pouvez 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() ? :has() présente certaines restrictions. Les principaux problèmes sont dus aux baisses de performances.

  • Vous ne pouvez pas :has() un :has(). Vous pouvez toutefois enchaîner un :has(). css :has(.a:has(.b)) { … }
  • Aucun pseudo-élément utilisé dans :has() css :has(::after) { … } :has(::first-letter) { … }
  • Limiter l'utilisation de :has() dans les pseudos n'acceptant que des 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 fausse. css :has(:visited) { … }

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

Et voilà !

Préparez-vous pour :has(). Parlez-en à vos amis et partagez cet article. Il va changer notre façon d'aborder le CSS.

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