Retour aux articles
25 Décembre 2025

CSS : les pseudo-classes modernes à connaître

CSS : les pseudo-classes modernes à connaître
CSS

Pourquoi ces pseudo-classes comptent

Pendant longtemps, certains comportements d'interface obligeaient à écrire du JavaScript — même pour des choses purement visuelles. "Styliser une carte quand une case cochée à l'intérieur change d'état", "cibler un parent selon son contenu"... c'étaient des cas typiques où CSS seul ne suffisait pas.

Les pseudo-classes modernes changent la donne. Elles couvrent des besoins réels et fréquents, sans une seule ligne de script.

Le fil conducteur de cet article : chaque pseudo-classe présentée ici remplace un pattern que tu as probablement déjà résolu avec un useState + un onChange côté React. Moins de JS, moins de re-renders, un rendu plus rapide.

:has() — le sélecteur parent

C'est la pseudo-classe la plus attendue de ces dernières années. Jusqu'ici, CSS pouvait cibler un enfant selon son parent (.card p), mais jamais l'inverse. :has() comble ce manque : elle sélectionne un élément selon ce qu'il contient.

css
/* Stylise la carte entière si elle contient une checkbox cochée */
.card:has(input:checked) {
	border-color: #1d9e75;
	background-color: #e1f5ee;
}

Tu viens de voir cette exacte démo plus haut dans la conversation : les trois cartes changent de couleur au clic, sans le moindre onClick ni useState — juste une règle CSS.

Autre cas très courant : styliser un champ de formulaire différemment selon qu'il contient une erreur ou non, en combinant :has() avec :invalid :

css
/* Le conteneur du champ devient rouge si l'input est invalide */
.form-field:has(input:invalid) {
	border-color: #d64545;
}

.form-field:has(input:invalid) .form-field__error {
	display: block;
}

Applicable directement sur ton ContactForm.tsx : tu peux styliser le conteneur .form-field autour de chaque input sans avoir à gérer un état d'erreur en React pour chaque champ, juste avec la validation HTML native (required, type="email"...).

:has() fonctionne aussi comme sélecteur de proximité, pour cibler un élément selon ce qui le suit :

css
/* Ajoute une marge à un titre seulement s'il est suivi d'un paragraphe */
h2:has(+ p) {
	margin-bottom: 0.5rem;
}

:is() et :where() — factoriser des sélecteurs

Ces deux pseudo-classes regroupent plusieurs sélecteurs en une seule règle, pour éviter la répétition. Sans elles, cibler plusieurs titres à l'intérieur de plusieurs conteneurs oblige à tout écrire en toutes lettres :

css
/* Sans :is() — répétitif */
.article-detail h1,
.article-detail h2,
.article-detail h3,
.project-detail h1,
.project-detail h2,
.project-detail h3 {
	font-family: var(--font-heading);
}

/* Avec :is() — factorisé */
:is(.article-detail, .project-detail) :is(h1, h2, h3) {
	font-family: var(--font-heading);
}

La différence entre les deux tient uniquement à la spécificité CSS — ce qui détermine quelle règle l'emporte en cas de conflit :

:is()

  • Prend la spécificité de son argument le plus fort
  • Peut écraser d'autres règles existantes
  • À utiliser quand la priorité normale est voulue

:where()

  • A toujours une spécificité de zéro
  • Ne peut jamais écraser une autre règle
  • Idéal pour des styles de base facilement surchargeables
css
/* :where() a une spécificité nulle : n'importe quelle autre règle
   peut la surcharger sans avoir besoin d'un sélecteur plus précis */
:where(.article-card, .project-card) {
	border-radius: 16px;
}

/* Cette règle plus simple suffit à surcharger la précédente */
.article-card {
	border-radius: 20px;
}

:focus-visible — un focus qui respecte l'utilisateur

Le vieux problème du focus CSS : :focus s'applique aussi bien à un clic souris qu'à une navigation clavier — ce qui pousse beaucoup de sites à masquer le contour de focus par défaut (outline: none), cassant au passage l'accessibilité pour les utilisateurs clavier.

:focus-visible résout ça proprement : le navigateur décide lui-même, selon le contexte, si le focus doit être visible ou non.

css
/* Retire l'anneau de focus disgracieux au clic souris... */
button:focus {
	outline: none;
}

/* ...mais le restaure proprement à la navigation clavier */
button:focus-visible {
	outline: 2px solid var(--primary);
	outline-offset: 2px;
}

Ne fais jamais outline: none sans le compenser par :focus-visible. C'est l'une des erreurs d'accessibilité les plus fréquentes — elle rend un site inutilisable au clavier, ce qui touche directement les critères RGAA que tu appliques sur tes projets.

:focus-within — styliser un conteneur entier

Autre besoin classique en formulaire : mettre en valeur tout un groupe de champs dès qu'un utilisateur interagit avec l'un d'eux à l'intérieur. :focus-within s'applique à un parent dès qu'un de ses descendants reçoit le focus.

css
/* Le conteneur entier réagit dès qu'un input à l'intérieur est actif */
.form-group:focus-within {
	border-color: var(--primary);
	box-shadow: 0 0 0 3px hsla(var(--primary-hsl), 0.15);
}

C'est le comportement exact qu'on retrouve sur beaucoup de formulaires modernes : le cadre entier "s'illumine" légèrement quand tu commences à taper, sans que chaque input individuel ait besoin de son propre style de focus.

:nth-child(An+B of S) — cibler dans un sous-ensemble

:nth-child() seule cible une position selon tous les enfants d'un parent. La syntaxe étendue of S restreint ce calcul à un sous-ensemble filtré — très utile sur une liste hétérogène.

css
/* Cible le 2e élément .featured parmi les enfants, en ignorant les autres */
.grid > :nth-child(2 of .featured) {
	grid-column: span 2;
}

Exemple concret pour une grille d'articles où certains sont marqués comme "à la une" : sans of S, :nth-child(2) cible le 2e enfant peu importe son type. Avec of .featured, il cible précisément le 2e élément parmi ceux qui ont la classe .featured — les cartes normales entre les deux n'influencent pas le calcul.

:not() avec plusieurs arguments

Depuis la spécification Selectors Level 4, :not() accepte une liste de sélecteurs séparés par des virgules, plutôt qu'un seul argument à la fois.

css
/* Avant : fallait chaîner plusieurs :not() */
.card:not(.featured):not(.archived):not(.draft) {
	opacity: 1;
}

/* Maintenant : une seule pseudo-classe, une liste */
.card:not(.featured, .archived, .draft) {
	opacity: 1;
}

Sur une grille de composants, ça permet d'exclure proprement plusieurs états sans empiler les sélecteurs — plus lisible, surtout quand la liste des exceptions grandit avec le temps.

Compatibilité navigateurs

Pseudo-classe Support Remarque
:has() Tous navigateurs modernes depuis fin 2023 Safari a été précurseur, Chrome et Firefox ont suivi
:is() / :where() Large support depuis 2021 Aucune restriction notable aujourd'hui
:focus-visible Large support depuis 2021 À privilégier systématiquement sur :focus seul
:focus-within Support quasi universel depuis longtemps La plus ancienne de cette liste
:nth-child(of S) Support récent, encore partiel Vérifier sur caniuse.com avant un usage critique
:not() multi-arguments Large support depuis 2023 Fonctionne partout où :has() fonctionne

Pour un projet grand public avec des exigences de compatibilité strictes, garde le réflexe de vérifier caniuse.com avant d'utiliser :has() ou la syntaxe of S comme mécanisme critique — mais pour un effet purement esthétique, un dégradé sans plantage sur les navigateurs plus anciens (l'élément reste juste non stylisé) est généralement acceptable.

En résumé

Pseudo-classe Remplace souvent
:has() Un onChange + useState juste pour du style conditionnel
:is() / :where() Des listes de sélecteurs répétitives
:focus-visible Un outline: none qui casse l'accessibilité clavier
:focus-within Un état de focus géré manuellement sur un groupe de champs
:nth-child(of S) Un filtrage + comptage fait en JavaScript
:not() multi-args Plusieurs :not() chaînés

Le point commun de toutes ces pseudo-classes : elles permettent d'exprimer directement en CSS des logiques qui, il y a quelques années encore, nécessitaient forcément du JavaScript. Moins de code, moins de state à synchroniser, et des interfaces qui restent réactives même si le script n'a pas encore fini de charger.