Face à un champ de formulaire requis laissé vide, deux approches s’opposent depuis toujours : écouter l’événement de validation en JavaScript et ajouter une classe à la main, ou laisser le CSS observer directement l’état natif :invalid du champ. La pseudo-classe :has() ouvre une troisième voie, moins connue : styliser le parent — ici un fieldset — en fonction de ce qu’il contient, sans jamais toucher au script.
Cette recette montre comment signaler visuellement un regroupement de champs contenant une erreur de validation, uniquement en CSS. Elle ne traite pas de la rédaction des messages d’erreur eux-mêmes, qui font l’objet d’un tout autre sujet, à savoir la manière de les formuler et de les annoncer.
Le problème posé
Sur un formulaire d’inscription, un fieldset regroupe trois champs liés à l’adresse postale. Si l’un des trois est requis et resté vide à la tentative de soumission, l’intégrateur souhaite entourer l’ensemble du regroupement d’un liseré visuellement distinct, en plus du message d’erreur associé à chaque champ. Une solution classique ajouterait un écouteur submit en JavaScript, qui parcourt les champs invalides et ajoute une classe sur leur fieldset parent :
document.querySelector( 'form' ).addEventListener( 'submit', function ( evenement ) {
document.querySelectorAll( 'fieldset' ).forEach( function ( groupe ) {
var contientInvalide = groupe.querySelector( ':invalid' );
groupe.classList.toggle( 'wpm-groupe-erreur', Boolean( contientInvalide ) );
} );
} );
Ce script fonctionne, mais il ajoute une dépendance à un événement précis, et doit être dupliqué ou adapté pour chaque nouveau type d’interaction — une saisie en temps réel, par exemple, demanderait d’écouter aussi input sur chaque champ.
La solution en pur CSS avec :has()
La pseudo-classe :has() sélectionne un élément si l’un de ses descendants correspond au sélecteur passé en argument. Appliquée à un fieldset, elle permet de le cibler directement s’il contient un champ à l’état natif :invalid, sans écouteur d’événement :
fieldset:has(:invalid) {
border-color: #b3261e;
background-color: #fdf2f1;
}
fieldset:has(:invalid) legend {
color: #b3261e;
font-weight: 600;
}
L’état :invalid reflète en continu la validité native du champ, telle que le navigateur la calcule à partir des attributs required, pattern ou type="email". Le style du fieldset se met donc à jour automatiquement, à chaque frappe, sans qu’aucun script n’ait besoin d’écouter quoi que ce soit.

La réserve de compatibilité qui compte
En janvier 2023, deux moteurs de rendu majeurs supportent :has() en production : Safari depuis la version 15.4, et les navigateurs basés sur Chromium depuis la version 105. Firefox, à cette date, ne le supporte pas encore en dehors d’un indicateur expérimental. Présenter :has() comme universellement disponible serait inexact : il s’agit d’une amélioration progressive, à réserver aux navigateurs qui la comprennent, jamais du seul mécanisme de signalement d’erreur.
Le repli pour les navigateurs qui ne supportent pas :has()
La requête de fonctionnalité @supports permet d’isoler proprement la règle, et de la faire cohabiter avec le script de repli déjà montré plus haut, qui continue de fonctionner partout :
@supports selector(:has(*)) {
fieldset:has(:invalid) {
border-color: #b3261e;
background-color: #fdf2f1;
}
}
Avec cette structure, un navigateur qui comprend :has() applique le style enrichi sans exécuter le moindre JavaScript supplémentaire ; un navigateur qui ne le comprend pas encore continue de recevoir le même signalement, via le script submit classique, sans dépendre d’une fonctionnalité CSS absente.
Variantes utiles
La même pseudo-classe permet aussi de cibler un label associé à un champ invalide, à condition que la structure HTML le permette, ou de styliser une ligne de tableau contenant une cellule en erreur dans un formulaire tabulaire. Une variante fréquente ajoute également un état positif, pour confirmer visuellement qu’un groupe de champs est désormais valide après correction :
fieldset:has(:valid):not(:has(:invalid)) {
border-color: #2e7d32;
}
Conseil maison : sur un projet où le support de Firefox reste indispensable à court terme, garder le script JavaScript comme mécanisme principal et traiter
:has()comme un simple raffinement visuel évite toute divergence de comportement entre navigateurs.
Notre verdict
Comparée à l’écoute d’événements JavaScript, la pseudo-classe :has() simplifie nettement le signalement visuel d’un groupe de champs en erreur, en s’appuyant directement sur l’état de validité natif du navigateur. Elle mérite sa place dans une feuille de style dès aujourd’hui, à condition de l’envelopper dans une requête @supports et de conserver un mécanisme de repli tant que l’ensemble des navigateurs ciblés ne l’a pas rattrapée.