Plusieurs problèmes qui exigeaient autrefois des contournements en JavaScript ont désormais des solutions natives en CSS.
Le point commun de cette liste : chaque fonctionnalité remplace un contournement, qu’il s’agisse d’un resize observer, d’une classe basculée par script, d’un préprocesseur ou d’une astuce de spécificité. La plupart sont désormais prises en charge par tous les grands moteurs de navigateur. Lorsque la prise en charge reste inégale, nous le signalons, avec une solution de repli raisonnable.
Les container queries
Les container queries ont profondément transformé le responsive design. Au lieu de dépendre uniquement des dimensions du viewport, les composants peuvent désormais s’adapter à la taille de leur conteneur parent. Cela rend possibles des composants réellement réutilisables : une carte qui réagit à l’espace qu’elle occupe plutôt qu’à la largeur de l’écran.
.card-container {
container-type: inline-size;
}
@container (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
Deux détails rendent les container queries plus utiles en pratique.
Nommez vos conteneurs. Les conteneurs imbriqués sont fréquents dans les vraies mises en page, et une requête @container sans nom cible le conteneur ancêtre le plus proche. Le nommage lève l’ambiguïté :
.sidebar {
container: sidebar / inline-size;
}
@container sidebar (min-width: 320px) {
.nav-item { flex-direction: row; }
}
Utilisez les unités de container query. cqi vaut 1 % de la taille inline du conteneur : font-size: clamp(1rem, 4cqi, 1.5rem) fait varier un titre avec sa carte plutôt qu’avec la fenêtre.
Les container style queries constituent l’étape suivante : @container style(--variant: compact) permet à un composant de réagir à une propriété personnalisée définie par un ancêtre, une alternative plus propre aux classes modificatrices. La prise en charge des style queries n’est pas encore universelle : conservez donc un comportement par défaut sans requête.
Une mise en garde : container-type: inline-size applique un confinement de taille sur l’axe inline, si bien que l’élément ne peut plus se dimensionner d’après la largeur de son contenu. Placez-le sur un wrapper dont la largeur vient de la mise en page, comme une cellule de grille, et non sur un élément qui s’ajuste à son contenu.
Le sélecteur :has()
Souvent appelé « sélecteur de parent », :has() permet des styles qui dépendent de l’état d’un descendant ou d’un élément voisin. Besoin de styliser différemment un groupe de formulaire lorsqu’il contient un champ invalide ? :has() rend cela trivial, sans intervention de JavaScript.
.field:has(input:user-invalid) {
--field-border: var(--color-danger);
}
.card:has(img) {
grid-template-rows: auto 1fr;
}
/* Bloque le défilement de la page tant qu'une boîte de dialogue modale est ouverte */
html:has(dialog[open]:modal) {
overflow: hidden;
}
La pseudo-classe :user-invalid de la première règle mérite d’être adoptée en parallèle : contrairement à :invalid, elle ne s’applique qu’après une interaction de l’utilisateur avec le champ, de sorte que les champs obligatoires vides ne s’affichent pas en erreur au chargement de la page.
:has() peut aussi regarder sur le côté. h2:has(+ p) cible un titre immédiatement suivi d’un paragraphe, ce qui est pratique pour ajuster l’espacement dans un texte. Ancrez vos sélecteurs sur un élément précis plutôt que d’écrire des tests body:has(...) sur l’ensemble du document, à la fois pour la lisibilité et pour limiter le travail d’invalidation du navigateur.
Les cascade layers
Les déclarations @layer donnent aux développeurs un contrôle explicite sur la cascade et mettent fin aux guerres de spécificité qui empoisonnent les feuilles de style depuis des décennies. Organisez vos styles en couches reset, base, composants et utilitaires pour obtenir un résultat prévisible, quel que soit l’ordre des sources.
@layer reset, base, components, utilities;
@layer components {
.button { background: var(--color-primary); }
}
@layer utilities {
.bg-transparent { background: transparent; }
}
L’ordre déclaré sur la première ligne fait foi : une règle de utilities l’emporte sur une règle de components, même lorsque le sélecteur du composant est plus spécifique. La spécificité ne compte qu’entre règles d’une même couche.
Deux comportements prennent souvent les développeurs au dépourvu :
- Les styles hors couche l’emportent sur tous les styles en couche pour les déclarations normales. Si vous importez une feuille de style tierce sans couche, elle écrasera les styles de vos composants placés en couche. Encapsulez-la plutôt :
@import url("vendor.css") layer(vendor);. !importantinverse l’ordre des couches. Une déclaration importante dans une couche précoce l’emporte sur une déclaration importante d’une couche ultérieure, ce qui permet à un reset de protéger délibérément une règle critique.
Tailwind CSS v4 repose sur les cascade layers natives, signe que les couches sont prêtes pour une architecture de production.
Subgrid
Les grilles de cartes ont longtemps souffert d’un défaut bien connu : des titres de longueurs différentes décalent le corps et le bouton de chaque carte à des hauteurs différentes. subgrid permet à une grille enfant d’adopter les pistes de son parent, de sorte que les éléments des cartes voisines s’alignent.
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
gap: 1.5rem;
}
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
}
Chaque carte s’étend sur trois rangées du parent et transmet ces rangées à ses enfants : tous les titres, corps et pieds d’une même rangée partagent ainsi la même hauteur. Subgrid hérite aussi par défaut de l’espacement (gap) du parent, que vous pouvez redéfinir sur l’enfant.
Couleur : oklch() et color-mix()
oklch() décrit une couleur par sa luminosité perceptuelle, sa chroma et sa teinte. Son avantage pratique sur hsl() est que des valeurs de luminosité égales paraissent réellement aussi claires les unes que les autres : une palette construite par paliers de luminosité se comporte donc de façon prévisible d’une teinte à l’autre. Elle permet aussi d’exprimer des couleurs dans des gamuts plus larges, comme Display P3.
:root {
--brand: oklch(62% 0.19 255);
--brand-hover: color-mix(in oklch, var(--brand), black 15%);
--brand-subtle: color-mix(in oklch, var(--brand) 12%, transparent);
}
color-mix() dérive des teintes plus claires, plus foncées et des variantes translucides à partir d’un seul token : un thème peut ainsi modifier une variable au lieu d’une dizaine. La syntaxe de couleur relative va plus loin et vous permet d’ajuster un seul canal d’une couleur existante : oklch(from var(--brand) calc(l + 0.1) c h) éclaircit une couleur sans toucher à sa teinte.
Associez-les à light-dark() et color-scheme: light dark pour définir les valeurs des deux thèmes en une seule déclaration, par exemple color: light-dark(#1a1a1a, #f2f2f2).
Les view transitions
L’API View Transitions apporte des animations de transition de page natives aux applications multipages. Combinée au composant <ClientRouter /> d’Astro, elle permet de créer une navigation fluide, digne d’une application, avec un minimum de code et un rendu soigné.
Il en existe deux variantes. Les transitions au sein d’un même document, lancées par script avec document.startViewTransition(), sont celles qu’utilisent les routeurs côté client. Les transitions entre documents ne nécessitent aucun JavaScript : les deux pages les activent en CSS.
@view-transition {
navigation: auto;
}
.post-hero {
view-transition-name: post-hero;
}
En donnant le même view-transition-name aux éléments correspondants de l’ancienne et de la nouvelle page, vous permettez au navigateur de transformer l’un en l’autre. Les transitions entre documents ne sont pas encore prises en charge par tous les moteurs, mais les navigateurs qui ne les supportent pas naviguent simplement de façon normale, ce qui en fait une amélioration progressive sans risque. Placez vos animations de transition personnalisées dans @media (prefers-reduced-motion: no-preference) pour que les utilisateurs qui demandent moins d’animations soient respectés.
L’imbrication
L’imbrication CSS native rend les préprocesseurs superflus dans de nombreux projets. Écrivez des styles qui reflètent la structure de votre HTML sans recourir à Sass ou Less, et gardez une chaîne d’outils plus légère.
.card {
padding: 1.5rem;
& h2 {
margin-block-end: 0.5rem;
}
&:hover {
box-shadow: var(--shadow-md);
}
@media (min-width: 48rem) {
padding: 2rem;
}
}
Il existe des différences avec Sass. Vous ne pouvez pas concaténer des sélecteurs : &__title, à la manière BEM, ne fonctionne pas. Et une règle imbriquée est traitée comme :is(), qui prend la spécificité de son argument le plus spécifique : imbriquer sous une liste de sélecteurs peut donc augmenter la spécificité d’une manière que Sass n’aurait pas. Gardez une imbrication peu profonde ; deux niveaux suffisent généralement.
La portée avec @scope
@scope limite un ensemble de règles à un sous-arbre, et peut éventuellement s’arrêter à une limite intérieure :
@scope (.card) to (.card-content) {
img { border-radius: 0.5rem; }
}
Ici, les images d’une carte ont des coins arrondis, mais celles placées dans le slot de contenu de la carte ne sont pas touchées. @scope ajoute aussi la notion de proximité de portée à la cascade : lorsque deux règles à portée limitée entrent en conflit à spécificité égale, celle dont la racine de portée est la plus proche de l’élément l’emporte. La prise en charge est arrivée dans Firefox plus tard que dans Chromium et Safari : vérifiez votre matrice de navigateurs avant de vous en servir pour quoi que ce soit de critique.
L’anchor positioning
Les infobulles et les menus déroulants nécessitaient traditionnellement une bibliothèque de positionnement en JavaScript. L’anchor positioning CSS permet à un élément de se positionner par rapport à un autre de manière déclarative :
.trigger {
anchor-name: --menu-trigger;
}
.menu {
position: absolute;
position-anchor: --menu-trigger;
position-area: block-end span-inline-end;
position-try-fallbacks: flip-block;
}
position-try-fallbacks indique au navigateur de basculer le menu au-dessus du déclencheur lorsqu’il n’y a pas de place en dessous, précisément le cas limite que les bibliothèques de positionnement existent pour gérer. Il se combine naturellement avec l’attribut popover pour la fermeture par clic extérieur. L’anchor positioning est d’abord arrivé dans Chromium et les autres moteurs ont suivi plus récemment : traitez-le comme une amélioration, conservez une position de repli simple et vérifiez la prise en charge actuelle avant d’abandonner une bibliothèque.
Des petits gains à adopter
Quelques fonctionnalités plus modestes méritent une place dans chaque nouvelle feuille de style :
text-wrap: balanceéquilibre la longueur des lignes dans les titres et évite un mot orphelin sur la dernière ligne.@propertyenregistre une propriété personnalisée avec un type, ce qui permet de l’animer, par exemple l’angle d’un dégradé.margin-inline,padding-block,inset-inline-startet les autres propriétés logiques adaptent les mises en page aux langues qui s’écrivent de droite à gauche sans règle supplémentaire.:focus-visibleaffiche le contour de focus aux utilisateurs du clavier sans l’afficher à chaque clic de souris.
Adopter les nouveautés en toute sécurité
La meilleure façon d’utiliser tout cela est de procéder par étapes. Vérifiez le statut Baseline d’une fonctionnalité avant d’en faire une exigence, et recourez à @supports lorsque la solution de repli nécessite des règles différentes :
@supports not (anchor-name: --a) {
.menu { inset-block-start: 100%; }
}
Les couches, les container queries, :has(), l’imbrication et les fonctions de couleur modernes sont aujourd’hui des fondations fiables. Les view transitions, @scope et l’anchor positioning sont d’excellentes améliorations. Ensemble, elles permettent à CSS de prendre en charge une large part du travail qui exigeait autrefois du JavaScript, avec moins de code à livrer et moins de code à maintenir.