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éscss ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
- Limiter l'utilisation de
:has()
après le pseudo-élémentcss ::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.