Chapitre 1123 minDébutant
CSS moderne : variables, nesting, :has() et animations
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- centraliser couleurs et espacements avec des variables CSS ;
- utiliser le nesting natif et les sélecteurs
:is(),:where()et:has(); - animer avec des transitions et des
@keyframes; - organiser une feuille de style et situer Bootstrap et Tailwind.
4 exercices corrigés
Le CSS moderne désigne les fonctionnalités arrivées ces dernières années dans tous les navigateurs, qui remplacent des outils autrefois indispensables (préprocesseurs comme Sass, scripts JavaScript) : les variables CSS, l'imbrication native (nesting), le sélecteur :has(), les propriétés logiques, les fonctions de calcul. Ce chapitre les présente avec les transitions et animations, puis donne une méthode pour organiser une feuille de style et un mot sur les frameworks Bootstrap et Tailwind.
Les variables CSS (custom properties)
Une propriété personnalisée, qu'on appelle couramment variable CSS, commence par deux tirets. On la déclare en général sur :root (l'élément <html>), pour qu'elle soit disponible partout, et on la lit avec var() :
:root {
--couleur-principale: #0f766e;
--couleur-texte: #1f2937;
--couleur-fond: #ffffff;
--rayon: 8px;
--espace: 1rem;
}
.bouton {
background-color: var(--couleur-principale);
border-radius: var(--rayon);
padding: calc(var(--espace) / 2) var(--espace);
}
h2 {
color: var(--couleur-principale);
}
C'est la traduction directe d'une charte graphique : les couleurs et espacements de l'organisation sont déclarés une fois, en tête de feuille. Le jour où la médiathèque change de vert, une seule ligne change.
Les variables suivent la cascade et l'héritage : on peut les redéfinir localement, et tous les descendants prennent la nouvelle valeur.
.zone-jeunesse {
--couleur-principale: #7c3aed; /* dans l'espace jeunesse, boutons et titres passent en violet */
}
var() accepte une valeur de secours, utilisée si la variable n'est pas définie : var(--ombre, none).
Les variables rendent aussi très simple un thème sombre qui suit le réglage du système :
:root {
color-scheme: light dark; /* les champs et barres de défilement suivent aussi le thème */
--couleur-texte: #1f2937;
--couleur-fond: #ffffff;
--couleur-principale: #0f766e;
}
@media (prefers-color-scheme: dark) {
:root {
--couleur-texte: #e5e7eb;
--couleur-fond: #111827;
--couleur-principale: #2dd4bf;
}
}
body {
color: var(--couleur-texte);
background-color: var(--couleur-fond);
}
Le reste de la feuille n'utilise que les variables : il n'y a rien d'autre à écrire pour le mode sombre. La fonction light-dark(#1f2937, #e5e7eb), reconnue par tous les navigateurs actuels depuis 2024, permet même d'écrire les deux valeurs sur une seule ligne, à condition d'avoir déclaré color-scheme: light dark.
Le nesting CSS natif
Le CSS sait désormais imbriquer des règles, comme le faisaient les préprocesseurs Sass ou Less. Le caractère & représente le sélecteur parent :
.carte {
padding: 1rem;
border: 1px solid #64748b;
& h3 { /* .carte h3 */
margin-top: 0;
}
&:hover,
&:focus-within { /* .carte:hover, .carte:focus-within */
border-color: var(--couleur-principale);
}
&.complet { /* .carte.complet (sans espace) */
opacity: 0.7;
}
@media (width >= 40rem) { /* une media query imbriquée */
padding: 1.5rem;
}
}
Le nesting est reconnu par tous les navigateurs actuels depuis fin 2023. Devant un sélecteur de type, le & est devenu facultatif (h3 { … } dans .carte donne .carte h3), mais l'écrire rend le code plus clair.
Les sélecteurs :is(), :where() et :has()
:is() et :where() regroupent des sélecteurs pour raccourcir l'écriture :
/* Au lieu de : article h2, article h3, aside h2, aside h3 */
:is(article, aside) :is(h2, h3) {
line-height: 1.2;
}
/* Les listes qui ont une classe (menus, listes de cartes) : tout est dans :where(), spécificité nulle, facile à surcharger */
:where(ul[class], ol[class]) {
list-style: none;
padding: 0;
}
La différence est la spécificité (chapitre 7) : :is() prend celle de son argument le plus fort, :where() vaut zéro.
:has(), reconnu par tous les navigateurs actuels depuis fin 2023, a changé la façon d'écrire du CSS : il sélectionne un élément selon ce qu'il contient ou selon ce qui le suit. On l'a longtemps appelé le « sélecteur de parent » qui manquait au CSS.
/* Les cartes qui contiennent une image : disposition en deux colonnes */
.carte:has(img) {
display: grid;
grid-template-columns: 8rem 1fr;
}
/* Le champ et son étiquette en rouge si le champ est invalide */
.champ:has(input:user-invalid) label {
color: #b91c1c;
}
/* Une option de choix encadrée quand sa case est cochée */
label:has(input:checked) {
outline: 2px solid var(--couleur-principale);
}
/* Le titre suivi directement d'un paragraphe de chapeau */
h2:has(+ p.chapeau) {
margin-bottom: 0.25rem;
}
Avant :has(), chacun de ces cas demandait une classe ajoutée à la main dans le HTML, ou un script.
Les propriétés logiques
Les propriétés « physiques » (margin-left, padding-top, width) parlent de gauche, droite, haut et bas. Les propriétés logiques parlent du sens de lecture :
- l'axe inline est celui dans lequel on lit une ligne (horizontal en français) ;
- l'axe block est celui dans lequel les paragraphes s'empilent (vertical en français) ;
- start et end désignent le début et la fin dans ce sens (gauche et droite en français, droite et gauche en arabe ou en hébreu).
| Physique | Logique |
|---|---|
margin-left et margin-right | margin-inline (ou margin-inline-start, margin-inline-end) |
padding-top et padding-bottom | padding-block |
width, height | inline-size, block-size |
top, right, bottom, left à 0 | inset: 0 |
text-align: left | text-align: start |
border-left | border-inline-start |
.contenu {
max-inline-size: 70rem;
margin-inline: auto; /* centré horizontalement */
padding-inline: 1rem; /* de l'air à gauche et à droite */
}
.citation {
border-inline-start: 4px solid var(--couleur-principale); /* trait au début de la ligne */
padding-inline-start: 1rem;
}
Si le site est un jour traduit dans une langue qui s'écrit de droite à gauche, ces règles s'adaptent seules ; margin-inline: auto est en plus plus court que margin-left: auto; margin-right: auto;.
calc(), min(), max() et clamp()
| Fonction | Rôle | Exemple |
|---|---|---|
calc() | un calcul mêlant des unités | width: calc(100% - 2rem); |
min() | la plus petite des valeurs | width: min(100%, 40rem); : 40rem au plus, jamais plus large que le parent |
max() | la plus grande | padding: max(1rem, 3vw); : au moins 1rem |
clamp() | une valeur bornée | font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem); |
Dans calc(), les opérateurs + et - doivent être entourés d'espaces : calc(100% -2rem) est invalide.
Transitions et animations
Une transition anime le passage d'une valeur à une autre quand un état change (survol, focus, ajout d'une classe) :
.bouton {
background-color: var(--couleur-principale);
transition: background-color 200ms ease, transform 200ms ease;
}
.bouton:hover,
.bouton:focus-visible {
background-color: #115e59;
transform: translateY(-2px);
}
transition prend la propriété animée, la durée, et une courbe d'accélération (ease, linear, ease-in-out). Évite transition: all, qui anime aussi des propriétés que tu ne voulais pas animer.
La propriété transform déplace, agrandit ou fait tourner un élément sans modifier la mise en page autour de lui : translate(10px, 0), scale(1.05), rotate(45deg). Pour une animation fluide, anime de préférence transform et opacity : le navigateur les traite sans recalculer la mise en page. Animer width, height, top ou margin force ce recalcul à chaque image et peut saccader.
Une animation se décrit par étapes avec @keyframes, et se joue sans changement d'état :
@keyframes apparition {
from {
opacity: 0;
transform: translateY(1rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-confirmation {
animation: apparition 400ms ease-out;
}
Organiser sa feuille de style
Un projet de quelques pages tient dans un fichier style.css, organisé en sections commentées, du général au particulier :
/* 1. Variables (charte graphique) */
:root { … }
/* 2. Base : remise à zéro et éléments HTML */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
line-height: 1.6;
color: var(--couleur-texte);
background-color: var(--couleur-fond);
}
img,
picture,
video,
svg {
display: block;
max-width: 100%;
height: auto;
}
input,
button,
textarea,
select {
font: inherit;
}
h1,
h2,
h3 {
line-height: 1.2;
text-wrap: balance; /* équilibre les lignes des titres sur plusieurs lignes */
}
/* 3. Mise en page : en-tête, grille de la page, pied */
/* 4. Composants : carte, bouton, formulaire, menu */
/* 5. Utilitaires : .visuellement-masque, .texte-centre */
La section 2 est une remise à zéro (reset) moderne : quelques règles qui gomment les différences entre navigateurs et les comportements gênants par défaut.
Pour nommer les classes, une convention évite les collisions dans les projets à plusieurs. La plus répandue est BEM (Bloc, Élément, Modificateur) :
<article class="carte carte--complet">
<h3 class="carte__titre">Initiation à l'ordinateur</h3>
<p class="carte__date">Mercredi 3 juin, 10 h</p>
<a class="carte__bouton" href="…">S'inscrire à l'initiation</a>
</article>
- le bloc est le composant autonome :
.carte; - l'élément est une partie du bloc, après deux soulignés :
.carte__titre; - le modificateur est une variante, après deux tirets :
.carte--complet.
Chaque sélecteur reste une classe unique, de spécificité (0,1,0) : la cascade se réduit à l'ordre d'écriture, et on sait d'un coup d'œil à quel composant appartient une classe.
Les frameworks CSS : Bootstrap et Tailwind
Un framework CSS fournit une feuille de style toute faite. Tu les croiseras en stage :
| Bootstrap | Tailwind CSS | |
|---|---|---|
| Principe | des composants prêts à l'emploi (.btn, .card, .navbar) et une grille | des classes utilitaires, une par propriété (p-4, text-center, bg-teal-700), combinées dans le HTML |
| Écriture | <button class="btn btn-primary"> | <button class="rounded bg-teal-700 px-4 py-2 text-white"> |
| Atout | rapide pour un back-office ou un prototype, documentation abondante | aucune feuille CSS à écrire, une interface cohérente, seules les classes utilisées sont générées |
| Limite | tous les sites Bootstrap se ressemblent ; il faut surcharger pour personnaliser | le HTML se charge de longues listes de classes ; une étape de construction est nécessaire |
Exercices
Exercice 1 sur 4 Passer à des variables Réussi À revoir
Réécris ce CSS avec des variables déclarées sur :root, puis ajoute un thème sombre qui suit le réglage du système (fond #111827, texte #e5e7eb, vert #2dd4bf).
body { color: #1f2937; background: #ffffff; }
h2 { color: #0f766e; }
.bouton { background: #0f766e; color: #ffffff; }
a { color: #0f766e; }
Voir la réponseMasquer la réponse de l'exercice 1
:root {
color-scheme: light dark;
--texte: #1f2937;
--fond: #ffffff;
--principale: #0f766e;
--sur-principale: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--texte: #e5e7eb;
--fond: #111827;
--principale: #2dd4bf;
--sur-principale: #111827;
}
}
body { color: var(--texte); background: var(--fond); }
h2 { color: var(--principale); }
.bouton { background: var(--principale); color: var(--sur-principale); }
a { color: var(--principale); }
Le texte du bouton a besoin de sa propre variable : du blanc sur le vert clair #2dd4bf serait illisible (contraste de 1,9:1 environ), alors que le fond sombre #111827 sur ce vert atteint 9,5:1.
Tu avais trouvé ?
Exercice 2 sur 4 Traduire en nesting Réussi À revoir
Réécris ces règles avec le nesting natif, dans un seul bloc .menu.
.menu { display: flex; gap: 1rem; }
.menu a { text-decoration: none; }
.menu a:hover, .menu a:focus-visible { text-decoration: underline; }
.menu a[aria-current="page"] { font-weight: 700; }
@media (width < 40rem) { .menu { flex-direction: column; } }
Voir la réponseMasquer la réponse de l'exercice 2
.menu {
display: flex;
gap: 1rem;
& a {
text-decoration: none;
&:hover,
&:focus-visible {
text-decoration: underline;
}
&[aria-current="page"] {
font-weight: 700;
}
}
@media (width < 40rem) {
flex-direction: column;
}
}
&:hover dans & a donne .menu a:hover : le & collé à la pseudo-classe ne crée pas d'espace. Le résultat a exactement les mêmes sélecteurs et la même spécificité que la version d'origine.
Tu avais trouvé ?
Exercice 3 sur 4 Écrire avec :has() Réussi À revoir
Écris un sélecteur pour chaque besoin.
- Mettre une bordure rouge à un
<fieldset>qui contient au moins un champ invalide après saisie. - Masquer le paragraphe
.aucun-resultatd'une liste.resultatsquand cette liste contient au moins un<li>. - Donner un fond coloré à la ligne de tableau
<tr>dont la case à cocher est cochée. - Agrandir le
<main>quand la page n'a pas d'<aside>(le<body>contientmainet éventuellementaside).
Voir la réponseMasquer la réponse de l'exercice 3
fieldset:has(:user-invalid) { border-color: #b91c1c; }.resultats:has(li) .aucun-resultat { display: none; }(si le paragraphe est dans.resultats) ; s'il le suit,.resultats:has(li) + .aucun-resultat.tr:has(input[type="checkbox"]:checked) { background-color: #ccfbf1; }body:not(:has(aside)) main { max-inline-size: none; }(ou une autre règle de largeur) ::not()et:has()se combinent.
Tu avais trouvé ?
Exercice 4 sur 4 Une animation respectueuse Réussi À revoir
Écris le CSS d'un bouton qui, au survol et au focus clavier, s'agrandit de 5 % en 150 ms, sans animer la mise en page. Ajoute ce qu'il faut pour les personnes qui demandent moins d'animations.
Voir la réponseMasquer la réponse de l'exercice 4
.bouton {
transition: transform 150ms ease;
}
.bouton:hover,
.bouton:focus-visible {
transform: scale(1.05);
}
@media (prefers-reduced-motion: reduce) {
.bouton {
transition: none;
}
}
transform: scale() agrandit le bouton sans pousser ses voisins ni recalculer la mise en page. Avec la réduction des animations demandée, le bouton s'agrandit toujours, mais sans mouvement progressif. Tu peux aussi écrire la règle à l'envers : @media (prefers-reduced-motion: no-preference) { .bouton { transition: … } }, l'animation n'étant alors ajoutée que pour ceux qui ne demandent rien.
Tu avais trouvé ?