Les pages marketing statiques et les sites de documentation livrent encore en 2026 des montagnes de HTML rédigé à la main. Jusqu’ici, styliser un parent selon l’état d’un enfant impliquait du JavaScript supplémentaire ou des classes enveloppes maladroites. Le pseudo-élément relationnel :has() inverse la logique : une section peut changer de bordure lorsqu’une case à l’intérieur est cochée, ou une ligne de formulaire peut se mettre en avant lorsqu’un champ est invalide—sans bundler et souvent sans une seule ligne de JS. En 2026, la couverture Safari 15.4+ est suffisante pour que de nombreuses équipes adoptent :has() pour des affordances visuelles, à condition de valider dans le WebKit réel et de maîtriser la complexité des sélecteurs. Ce guide couvre des motifs pratiques, une matrice de décision face à JS et aux requêtes conteneur, et l’intégration de contrôles dans un flux QA Safari sur un Mac mini loué.
Ce que :has() corrige sur les pages statiques
Le CSS classique pouvait styler les descendants selon les ancêtres (.theme-dark .card) mais pas l’inverse. Les équipes produit contournait avec des classes dupliquées basculées par React ou des attributs data-* miroir de l’état enfant. Sur les sites statiques sans JS—mentions légales, microsites de référence API, pages d’événement—cette friction bloquait des finitions UX subtiles. :has() exprime l’intention directement : « cette carte est en erreur parce qu’elle contient .error-text. » Les équipes accessibilité attendent toujours des libellés visibles et de l’ARIA là où il faut ; :has() gère la décoration, pas le remplacement de la sémantique.
Parce que :has() est déclaratif, les designers prototypent dans CodePen et livrent les mêmes sélecteurs via Eleventy ou Hugo sans étape d’hydratation. Le compromis est mental : les sélecteurs relationnels sont puissants et faciles à trop imbriquer. Fixez une règle maison du type « pas plus de deux combinateurs après :has() dans les gabarits marketing » pour que les mainteneurs futurs puissent grep sereinement.
Évitez d’ancrer :has() sur des nœuds Portal qui montent et descendent souvent ; même sur du statique, une couche de recherche peut restructurer le DOM. Ancrez plutôt sur des cartes, groupes de champs ou sections de tableau stables. Les sites multilingues doivent tester chaque langue : la longueur des traductions déplace les correspondances de sélecteurs.
Modèles de syntaxe prêts à l’emploi
Commencez par des composants autonomes. Un groupe de champs qui doit rayonner lorsqu’un contrôle est focalisé :
.field-group:has(:focus-visible) {
box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.35);
}
Une ligne de tableau qui signale des traductions manquantes lorsqu’une cellule porte un jeton :
tr:has(td[data-missing="true"]) {
background: rgba(255, 59, 48, 0.08);
}
Combinez avec :not() pour des états d’exclusion—par exemple désactiver visuellement un envoi tant que les champs requis sont vides—tout en gardant à l’esprit que la détection de vide en CSS pur reste limitée face à l’API Constraint Validation. Lorsque les règles métier dépassent les sélecteurs d’attributs, associez un minuscule script à des classes plutôt qu’une chaîne :has() à dix clauses.
Avec un thème sombre, :has() peut coexister avec color-scheme et prefers-reduced-motion pour atténuer les ombres lorsqu’un média enfant est actif et que l’utilisateur réduit les animations. Veillez à l’ordre en cascade avec les media queries pour que des feuilles chargées tard ne cassent pas l’intention.
Chronologie Safari et notes de test
Faits concrets pour votre matrice :
- Safari 15.4 (mars 2022) a livré :has() sur macOS 12.3 et iOS/iPadOS 15.4, dans la foulée du support Chromium 105 sur d’autres moteurs.
- Des politiques d’entreprise figent encore macOS avant 12.3 ; si 3 à 4 % du chiffre provient de ces builds, proposez une couleur de bordure de repli sans :has().
- Des correctifs WebKit arrivent parfois d’abord dans Safari Technology Preview ; comparez stable et STP lorsqu’un bug concerne nth-child dans :has(), puis retestez chaque déploiement statique mensuel.
Documentez la version minimale de Safari dans le README à côté de Node et pnpm pour que les prestataires ne « réparent » pas les mises en page en supprimant :has() sous pression. Réévaluez cette ligne tous les six mois quand le parc macOS se renouvelle.
Avec un CDN canary, injectez un CSS simplifié pour les Safari anciens plutôt que de dupliquer tout le HTML. Traitez :has() comme un enrichissement : sans lui, le contenu reste lisible et les formulaires soumissibles.
Matrice : :has(), JS ou @container
| Besoin | Préférer | Pourquoi |
|---|---|---|
| Mettre en avant le parent si l’enfant est invalide | :has(:invalid) | Zéro JS, fonctionne hors ligne en HTML statique. |
| Réordonner les pistes de grille quand la sidebar change de largeur | @container | La mise en page pilotée par la largeur relève des requêtes conteneur, pas de :has(). |
| Publier du JSON de formulaire avec erreurs serveur | JavaScript | Réseau et mises à jour ARIA live dépassent le périmètre CSS. |
| Afficher une icône si une case est cochée | :has(:checked) | Déclaratif et accessible si les labels sont corrects. |
| Limiter la télémétrie de profondeur de scroll | JavaScript | Le CSS ne peut pas déclencher des balises de façon sûre. |
Quand :has() et @container s’appliquent tous deux, séparez les rôles : conteneurs pour la macro-mise en page, :has() pour l’état micro à l’intérieur du composant. Empiler les deux lourdement sur le même élément aide rarement la lisibilité.
Associé aux requêtes conteneur, un schéma qui indique qui réagit à la boîte parent et qui à l’état enfant accélère les revues de code.
Performance et hygiène des sélecteurs
Les navigateurs réévaluent les sélecteurs relationnels lorsque les descendants changent. Sur une longue page à plus de 4 000 nœuds, body:has(.modal[open]) { overflow: hidden; } passe en général, mais chaîner plusieurs :has() sur des animations à haute fréquence, non. L’invalidation de style WebKit est efficace sur des sous-arbres localisés ; gardez les racines :has() près des cartes et sections de formulaire. Si l’onglet Performance montre des barres violettes de recalcul supérieures à 2–3 ms sur portable milieu de gamme pendant des tempêtes de survol, passez à une classe unique sur un wrapper basculée par un écouteur délégué.
Des linters Stylelint avec des règles de spécificité évitent la « soupe de sélecteurs ». Une étape CI sur Mac cloud fait remonter les régressions propres à macOS avant fusion ; louer du Silicon pour quinze jours évite d’acheter un cinquième MacBook pour une campagne courte.
Sécurité : :has() ne lit pas le texte arbitraire—seulement structure et pseudo-classes—donc ce n’est pas un canal d’exfiltration en soi. Évitez néanmoins d’envoyer des chaînes complètes de sélecteurs avec des classes générées par les utilisateurs vers des analytics tiers.
Lors de la migration de modificateurs BEM comme .card--error, planifiez deux sprints de recouvrement : gardez la classe pour les hooks analytiques pendant que :has() pilote le visuel, puis supprimez le modificateur quand le tracking migre vers des attributs data. Les sites statiques gagnent souvent 2–4 Ko gzip en retirant des scripts de bascule redondants.
Si vous stylisez l’impression avec :has(), testez l’aperçu d’impression Safari : les PDF d’archivage peuvent masquer des bordures d’erreur sur fond blanc.
Liste QA sur un Mac mini cloud
Louez un hôte macOS avec Safari stable, chargez votre build statique via file:// ou un serveur local, et parcourez trois flux : navigation clavier pure dans les formulaires, états invalides forcés, zoom à 200 % pour détecter les débordements. Enregistrez en 1280×720 sans HAR pour les sessions de chasse aux bugs. Sans Mac locaux, SSH et un VNC occasionnel suivent le même guide que nos autres articles Safari.
Les Mac mini Apple Silicon restent silencieux sous lint parallèle et profilage de recalcul de styles, utile pour traiter vingt gabarits HTML chaque nuit. L’accès cloud permet à des prestataires de partager une machine plutôt que d’expédier des ordinateurs.
Ajoutez une étape de fumée automatisée : charger la page, basculer chaque champ exemple entre valide et invalide, capturer le panneau des styles calculés pour les racines :has(). Ces PNG coûtent peu d’espace mais économisent 25–35 minutes de débat lorsque WebKit diverge de Chromium.
FAQ
Quelle version de Safari inclut :has() ?
Safari 15.4 sur macOS 12.3 et iOS 15.4 (mars 2022). Les équipes sur Safari 14 ont besoin d’un repli sans :has().
:has() remplace-t-il JavaScript pour les formulaires ?
Pour des indices purement visuels (bordures, icônes) souvent oui ; pour les messages de validation, les régions ARIA live ou les allers-retours serveur, il faut encore du JS ou une sémantique HTML au-delà du CSS.
:has() nuit-il aux performances ?
Des chaînes profondément imbriquées sur d’immenses pages peuvent augmenter le coût de recalcul. Localisez les sélecteurs, évitez de lier :has() à chaque survol sur une page marketing à 5k nœuds, et mesurez dans le Web Inspector.
Avec discipline, :has() retire du boilerplate du HTML statique tout en gardant des bundles légers. Associez de vraies sessions Safari—pas seulement Chromium—pour attraper les particularités d’invalidation WebKit. Un Mac mini Apple Silicon offre WebKit natif, faible consommation au repos et silence pour de longues sessions QA manuelles. MacHTML loue des Mac mini physiques avec SSH/VNC pour monter un labo WebKit le temps d’une release, puis réduire l’échelle quand la campagne se termine—sans CAPEX pour du matériel inactif entre deux lancements.
Besoin d’un matériel Safari pour la QA :has() ?
Louez un Mac mini Apple Silicon, exécutez Web Inspector sur le WebKit réel et continuez à livrer du HTML statique depuis votre éditeur habituel.