Module 4 · Fondations du web

CSS 2 — grid, responsive, variables

Dans le module précédent (le modèle de boîte & flexbox), tu as appris à aligner des boîtes le long d'une ligne. Ce module ajoute les trois outils qui font le reste d'une vraie mise en page moderne : grid pour poser une grille en deux dimensions, le responsive pour que la page s'adapte du téléphone à l'écran géant, et les variables CSS pour ranger couleurs et tailles à un seul endroit — exactement comme les design tokens de ton app mobile.

💡 Les trois idées du module
  • Grid : flexbox raisonne en une dimension (une rangée OU une colonne) ; grid raisonne en deux (des rangées ET des colonnes en même temps). Ce n'est pas un remplaçant de flexbox, c'est son complément.
  • Responsive : une seule page, plusieurs largeurs d'écran. Le web résout ça dans le CSS (les media queries : des blocs de style qui ne s'appliquent qu'à certaines largeurs d'écran), là où React Native le résout en JS — deux philosophies inverses pour le même problème.
  • Variables CSS : --couleur-primaire déclarée une fois, utilisée partout via var(...). C'est le mécanisme natif du navigateur pour faire ce que Colors.* et FONT_SIZE_* font dans ton app.

Grid : la deuxième dimension

Flexbox est un outil unidimensionnel : un conteneur, une direction, des enfants alignés le long de cet axe unique. Parfait pour une barre de navigation ou une ligne « icône + texte + badge ». Mais dès qu'on veut un quadrillage — trois cartes par rangée, alignées horizontalement et verticalement — flexbox rame : on peut le forcer avec flex-wrap et des largeurs calculées à la main, mais chaque rangée reste indépendante des autres, et rien ne garantit que les colonnes s'alignent.

CSS Grid est l'outil pensé pour ce cas. L'idée en une phrase : sur le conteneur, tu déclares une grille — combien de colonnes, quelles largeurs — et les enfants se rangent dans les cases, rangée après rangée. Le parent décide de la structure, les enfants la remplissent : un renversement par rapport à flexbox, où les enfants gardent l'initiative sur leur taille.

Deux propriétés suffisent pour démarrer. display: grid transforme le conteneur en grille ; grid-template-columns décrit les colonnes — une largeur par colonne, et le nombre de valeurs donne le nombre de colonnes. Exemple sur la page d'accueil de RendezVous, la clinique fictive du guide, et sa grille de cartes de services :

styles/services.css
/* Le conteneur des cartes de services devient une grille de 3 colonnes. */
.services {
  display: grid;                        /* j'active le mode grille */
  grid-template-columns: 1fr 1fr 1fr;   /* 3 colonnes de largeur égale */
  gap: 16px;                            /* l'espace ENTRE les cases (lignes ET colonnes) */
}
/* Les enfants (.service-card) n'ont RIEN à déclarer :
   ils se rangent tout seuls dans les cases, de gauche à droite,
   puis passent à la rangée suivante. */

Trois lignes de CSS sur le parent, zéro ligne sur les enfants : c'est la signature de grid.

Arrête-toi sur cette unité étrange : fr, pour fraction — une part de l'espace libre. 1fr 1fr 1fr découpe l'espace disponible en trois parts égales, et on peut mélanger : 200px 1fr donne une colonne fixe (une barre latérale) et une colonne qui prend tout le reste ; 2fr 1fr donne une première colonne deux fois plus large. Tu reconnais l'esprit de flex: 1 — distribuer l'espace restant par parts — mais déclaré sur le parent, colonne par colonne. Dernier raccourci : repeat(4, 1fr) dit la même chose que 1fr 1fr 1fr 1fr. Du confort seulement… jusqu'à la section suivante, où repeat() révèle un super-pouvoir.

grid-template-columns — mêmes enfants, trois déclarations
1fr 1fr 1fr
1
2
3
4
5
6
2fr 1fr
1
2
3
4
96px 1fr — une fixe, une qui prend le reste
côté
contenu principal
côté
contenu principal
Aucun enfant ne déclare quoi que ce soit : c'est le parent qui pose la grille, et le nombre de valeurs donne le nombre de colonnes. Les fr se partagent l'espace libre — 2fr 1fr donne deux tiers puis un tiers — tandis qu'une largeur en pixels est prélevée d'abord, le 1fr ramassant tout le reste. C'est le patron de la barre latérale.
🧭 Bon à savoir

Le gap est exactement celui du module précédent, avec le même rôle en grid : l'espace entre les cases, jamais autour de la grille. Il existe aussi grid-template-rows pour dicter la hauteur des rangées, mais en pratique on la laisse s'ajuster au contenu — sur le web, la hauteur découle du contenu.

🧠 Quiz éclair

La grille est déclarée à trois colonnes, et les neuf cartes s'empilent pourtant les unes sous les autres, dans une colonne étroite calée à gauche : les deux tiers droits de la section restent vides. Le CSS est correct — où est le problème ?

.services {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}
<section class="services">
  <div class="services__interieur">
    <article class="service-card">…</article>
    <article class="service-card">…</article>
    <!-- … sept autres cartes … -->
  </div>
</section>

Une grille ne range que ses enfants directs, et .services n'en a qu'un seul : le div.services__interieur. C'est donc lui, et lui seul, qui est un item de la grille : il occupe la première case, large d'un tiers (1fr sur trois), pendant que les colonnes 2 et 3 restent vides faute de candidats. Les neuf cartes, elles, sont des petites-filles de la grille — elles ne la voient même pas et s'empilent dans le flux normal à l'intérieur du div.

Deux correctifs, au choix : supprimer le div intermédiaire, ou déplacer les trois déclarations sur .services__interieur. La règle vaut à l'identique pour display: flex : un conteneur ne commande jamais qu'un seul niveau de descendance. Un wrapper glissé entre les deux — souvent par un composant React qui enveloppe ses enfants — suffit à couper la relation.

La grille responsive sans media query : la formule du module

La grille à trois colonnes de RendezVous a un défaut évident : sur un téléphone, trois colonnes dans 360 pixels donnent des cartes écrasées. Le réflexe classique serait une media query — un bloc de style qui ne s'applique qu'à partir (ou en dessous) d'une certaine largeur d'écran ; on les détaille deux sections plus bas. Mais grid offre plus élégant : une grille qui calcule elle-même son nombre de colonnes. Une seule ligne, probablement celle que tu recroiseras le plus souvent dans du vrai code — décortiquons-la.

grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));

Lis-la de l'intérieur vers l'extérieur. minmax(240px, 1fr) décrit une colonne élastique : « jamais plus étroite que 240 pixels, et sinon, prends une part égale de l'espace libre (1fr) ». Le minimum garantit la lisibilité — une carte de service ne descendra jamais sous 240 px — et le maximum en fr garantit que les colonnes se partagent équitablement toute la largeur, sans laisser de vide à droite.

auto-fit remplace le nombre fixe dans repeat() : au lieu de « répète 3 fois », « répète autant de fois que la place le permet ». Le navigateur calcule en continu — conteneur de 1000 px ? quatre colonnes (élargies à 250 px pour remplir) ; fenêtre à 700 px ? deux ; téléphone de 360 px ? une seule, pleine largeur. Les cartes passent de 4 à 3 à 2 à 1 toutes seules, sans un seul seuil écrit.

Un mot sur son jumeau, que tu croiseras autant : auto-fill. Les deux créent autant de colonnes que la place le permet, mais ils ne font pas la même chose des colonnes vides. auto-fill les garde — elles occupent leur largeur, invisibles, et les cartes présentes restent à leur taille minimale. auto-fit les effondre à zéro, et les cartes présentes s'étirent pour occuper toute la ligne. La différence ne se voit donc que lorsqu'il y a moins de cartes que de colonnes possibles : à six cartes dans une grille qui en tient quatre par ligne, les deux mots-clés donnent exactement la même page. C'est ce qui les rend faciles à confondre — et c'est aussi la réponse à « pourquoi mes deux cartes restent-elles serrées à gauche au lieu de remplir la ligne ? ». Le coupable est auto-fill ; le remède est auto-fit.

Mesure ce qui vient de se passer : tu as décrit une contrainte (« chaque carte a besoin d'au moins 240 px ») et le navigateur en a déduit la mise en page, pour toutes les largeurs présentes et futures. C'est la philosophie du CSS moderne — déclarer l'intention, laisser le moteur calculer — poussée au maximum. Beaucoup de grilles de cartes en production n'ont que ça comme stratégie responsive.

Une seule règle, trois largeurs de conteneur
conteneur de 258 px → 3 colonnes
1
2
3
4
5
6
conteneur de 160 px → 2 colonnes
1
2
3
4
5
6
conteneur de 86 px → 1 colonne
1
2
3
4
5
6
Les trois conteneurs portent la même déclaration — repeat(auto-fit, minmax(68px, 1fr)) — et les mêmes six enfants. Seule leur largeur change, et le nombre de colonnes passe de trois à deux puis à une, sans qu'aucun seuil ait été écrit nulle part. Le minimum est ici de 68 px au lieu de 240, et les conteneurs sont réduits d'autant, pour que les trois cas tiennent dans la page d'un téléphone ; le mécanisme, lui, est exactement le même.
📖 La formule

La grille qui respire

.services {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
}

La grille responsive sans une seule media query : « autant de colonnes d'au moins 240 px que la place le permet, qui se partagent le reste ». Apprends à la reconnaître d'un coup d'œil — elle est partout, des portfolios aux dashboards, et c'est souvent la première chose qu'on écrit dans un test technique de mise en page.

📅 Dans RendezVous

La section « Nos services » de la clinique utilise exactement cette formule : neuf cartes dans une .services en grille qui respire. Sur le téléphone d'un patient elles s'empilent en une colonne ; sur l'ordinateur de la réceptionniste elles s'étalent en trois ou quatre. Le fichier CSS n'a jamais entendu parler de « mobile » ni de « desktop » : il n'a déclaré qu'une contrainte de largeur minimale.

✍️ Exercice de lecture

Voici la grille des créneaux horaires de RendezVous :

.slots {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 8px;
}

Questions : (1) Dans un conteneur de 640 px de large, combien de colonnes la grille affiche-t-elle ? (2) Et dans un conteneur de 300 px ? (3) Que se passerait-il si on remplaçait 1fr par 150px dans le minmax ? (Pour les questions 1 et 2, ignore le gap : on raisonne en ordre de grandeur.)

Voir le corrigé

(1) Quatre colonnes : 640 ÷ 150 = 4,26, donc quatre colonnes de 150 px tiennent (mais pas cinq). Les quatre colonnes s'élargissent ensuite à 160 px chacune, grâce au 1fr, pour occuper toute la largeur.

(2) Deux colonnes : 300 ÷ 150 = 2 tout juste. Chaque colonne fait exactement 150 px. (Si on comptait vraiment le gap de 8 px, les deux colonnes de 150 px ne tiendraient plus dans 300 px et la grille retomberait à une seule colonne élargie : c'est exactement pour éviter ce calcul au pixel près que l'énoncé demandait de l'ignorer.)

(3) Avec minmax(150px, 150px), les colonnes seraient figées à 150 px : le bon nombre de colonnes apparaîtrait toujours, mais elles ne s'étireraient plus pour remplir la largeur — il resterait un vide à droite du conteneur. Le 1fr en maximum est ce qui fait « respirer » la grille.

Quand grid, quand flexbox : la règle simple

Question inévitable — on te la posera en entrevue, et tu te la poseras devant chaque mise en page. Une règle simple couvre 90 % des cas :

  • Le contenu dicte la disposition → flexbox. Tu as des éléments de tailles naturelles différentes (un logo, trois liens, un bouton) et tu veux juste les aligner le long d'un axe, en laissant chacun prendre sa place ? C'est du flex. La disposition découle du contenu.
  • La grille dicte la disposition → grid. Tu as une structure en tête avant de connaître le contenu — « trois colonnes », « une sidebar de 200 px et le reste », « des cartes alignées en quadrillage » — et le contenu devra s'y ranger ? C'est du grid. La disposition précède le contenu.

Autrement dit : flex part des enfants et les aligne ; grid part du parent et les case. Et surtout, ce n'est pas un duel — une vraie page mélange les deux en permanence, souvent l'un dans l'autre. Regarde la page d'accueil de RendezVous :

styles/accueil.css
/* La barre du haut : un logo, des liens, un bouton "Prendre rendez-vous".
   Des éléments de tailles différentes à aligner sur UNE ligne → flexbox. */
.topbar {
  display: flex;
  align-items: center;         /* centrage vertical sur la ligne */
  justify-content: space-between; /* logo à gauche, actions à droite */
}

/* La section des services : un quadrillage de cartes → grid (la formule !). */
.services {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
}

/* Et DANS chaque carte : une icône, un titre, une flèche, sur une ligne → re-flexbox. */
.service-card__entete {
  display: flex;
  align-items: center;
  gap: 8px;
}

Grid pour le quadrillage, flex pour les lignes — y compris des lignes flex à l'intérieur des cases de la grille. C'est l'imbrication normale d'une page réelle.

Au passage, .topbar n'a rien de neuf : c'est La rangée flexible du module CSS 1 — modèle de boîte & flexbox, recopiée telle quelle — display: flex, align-items: center, justify-content: space-between. Mêmes déclarations, même effet. Et .service-card__entete, deux niveaux plus bas, en est une seconde occurrence dans le même fichier, sans le justify-content puisqu'on ne veut rien pousser aux bords.

Un repère pour les cas limites : si tu te surprends à calculer des largeurs en pourcentage sur des enfants flex (width: 33.33%…) pour simuler des colonnes alignées, arrête-toi — tu réinventes grid avec les moyens du bord. Inversement, grid pour poser deux éléments côte à côte sans notion de colonnes est une surcharge : un flex suffisait. Aucun des deux choix ne casse la page, mais choisir l'outil qui correspond à l'intention rend le CSS bien plus lisible — et c'est cette intention qu'un recruteur écoute quand il pose la question.

Le responsive : une page, toutes les largeurs

Le responsive design, c'est l'idée qu'une même page reste utilisable du téléphone (environ 360 px) à l'écran de bureau (1400 px et plus). Ce n'est pas une option : plus de la moitié du trafic web mondial est mobile, et Google indexe les sites d'après leur version mobile. Historiquement on faisait deux sites (le tristement célèbre m.exemple.com) ; depuis les années 2010, une page dont le CSS s'adapte.

Première pièce, discrète mais indispensable : la balise viewport, dans le <head> de chaque page. Tu l'as déjà croisée :

<meta name="viewport" content="width=device-width, initial-scale=1">

Son histoire explique son existence. Quand l'iPhone est sorti, le web était pensé pour des écrans de 1000 px ; pour ne pas afficher des pages cassées, le navigateur mobile faisait semblant d'être large de 980 px puis dézoomait le tout — d'où ces sites minuscules qu'on pinçait pour zoomer. La balise dit : « arrête de faire semblant, utilise la vraie largeur de l'appareil, sans zoom initial ». Sans elle, toutes tes media queries mobiles sont court-circuitées. Si un site « ignore » ton CSS mobile, vérifie cette balise en premier.

Deuxième pièce, la principale : les media queries. Une media query est un bloc de CSS conditionnel : « ces règles ne s'appliquent que si l'écran remplit telle condition ». La condition la plus courante, de très loin, porte sur la largeur :

/* Ces règles s'appliquent TOUT LE TEMPS. */
.rendez-vous__panneau {
  padding: 24px;
}

/* Celles-ci s'appliquent SEULEMENT si la fenêtre fait 600 px de large ou moins. */
@media (max-width: 600px) {
  .rendez-vous__panneau {
    padding: 12px;            /* moins d'espace perdu sur petit écran */
  }
}

max-width: 600px se lit « largeur maximale 600 px », donc : « de 0 à 600 px ». Au-delà, le bloc est ignoré et seule la règle générale s'applique.

Reste une question de stratégie : dans quel sens écrire son CSS ? Le desktop-first écrit d'abord le style grand écran, puis des media queries en max-width qui retirent ou compactent pour les petits écrans. Le mobile-first fait l'inverse : le style de base est celui du téléphone — une colonne, tout simple — et des media queries en min-width ajoutent de la complexité quand la place augmente.

Mobile-first a gagné pour trois raisons. Robustesse : le style par défaut est le plus simple, et c'est lui qui s'applique si une media query échoue. Discipline : partir du mobile force à décider ce qui est essentiel (tout doit tenir dans 360 px) puis à enrichir, là où partir du desktop mène à caser tant bien que mal une page chargée dans un petit écran. Écosystème : Tailwind — donc NativeWind — est mobile-first par construction, puisque md:flex-row veut dire « à partir de la largeur md », un min-width déguisé. Tu pratiques déjà le mobile-first sans le savoir.

🔗 Pont — media queries ↔ useWindowDimensions

En React Native, pas de media queries CSS : on lit les dimensions en JavaScriptuseWindowDimensions() rend { width, height }, et on écrit if (width > 600) ... dans le composant. Le web fait l'inverse : l'adaptation est déclarée dans le CSS, le JavaScript n'est même pas au courant. RN adapte dans le code, le web dans le style — un chiasme qui fait une excellente réponse si on te demande en entrevue comment tu ferais du responsive côté mobile.

✍️ Exercice de lecture

Voici un extrait mobile-first du CSS de la page « Nos médecins » de RendezVous :

.medecins {
  display: grid;
  grid-template-columns: 1fr;   /* base : une colonne */
  gap: 12px;
}

@media (min-width: 700px) {
  .medecins {
    grid-template-columns: 1fr 1fr;
  }
}

@media (min-width: 1100px) {
  .medecins {
    grid-template-columns: repeat(3, 1fr);
    gap: 24px;
  }
}

Questions : (1) Combien de colonnes s'affichent à 900 px de large ? (2) Quel est le gap à 900 px ? (3) À 1200 px, les DEUX media queries « matchent » (1200 ≥ 700 et 1200 ≥ 1100) — laquelle gagne, et pourquoi ?

Voir le corrigé

(1) Deux colonnes : 900 est ≥ 700 (la première media query s'applique) mais < 1100 (la seconde est ignorée).

(2) 12 px. Seule la seconde media query redéfinit le gap, et elle ne s'applique pas encore à 900 px : c'est la valeur de base qui reste en vigueur. C'est tout l'esprit mobile-first — chaque palier ne redéfinit que ce qui change.

(3) À 1200 px, les deux blocs s'appliquent réellement, et ils entrent en conflit sur grid-template-columns. Comme les deux sélecteurs ont le même poids, c'est la règle écrite en dernier dans le fichier qui l'emporte (la cascade, vue dans le module sur le modèle de boîte) : trois colonnes. D'où l'importance d'écrire les media queries mobile-first dans l'ordre croissant des largeurs — inverser leur ordre dans le fichier casserait silencieusement le palier trois colonnes.

🧠 Quiz éclair

Sur le grand écran du poste d'accueil (1400 px), la liste des médecins s'obstine à n'afficher que deux colonnes. Le palier à trois colonnes est pourtant bien écrit, sans faute de frappe. Combien de colonnes s'affichent à 1400 px, et pourquoi ?

.medecins {
  display: grid;
  grid-template-columns: 1fr;
}

@media (min-width: 1100px) {
  .medecins { grid-template-columns: repeat(3, 1fr); }
}

@media (min-width: 700px) {
  .medecins { grid-template-columns: 1fr 1fr; }
}

Deux. À 1400 px, les deux media queries sont vraies en même temps (1400 ≥ 1100 et 1400 ≥ 700) : une media query n'est pas un else if, elle ne referme rien, c'est une simple condition d'application. Les deux blocs s'empilent donc, et ils se disputent la même propriété sur le même sélecteur. Les deux .medecins ayant exactement le même poids, c'est la cascade qui tranche — et à spécificité égale, la règle écrite en dernier gagne.

Le bloc « 700 » étant plus bas dans le fichier, il écrase le bloc « 1100 » partout où les deux s'appliquent : le palier trois colonnes est mort à toutes les largeurs, sans une erreur ni un avertissement. D'où la discipline mobile-first, qui n'est pas qu'une question de style : des min-width rangés par largeurs croissantes, pour que le palier le plus large soit toujours le dernier mot.

Les unités : px, %, rem, em, vh, vw — quoi utiliser où

Jusqu'ici on a écrit des px partout, mais le CSS offre tout un vocabulaire d'unités, et choisir la bonne fait partie de la lecture d'un code soigné. Pas de dogme — chaque unité a son terrain naturel :

UnitéRelative à…Terrain naturel, et ce qu'il faut savoir
pxrien (absolue)Bordures, arrondis, icônes, petits espacements : le géométrique, qui n'a pas de raison de grandir avec le texte.
%le parentLargeurs proportionnelles, images fluides (max-width: 100%). « Pourcentage de quoi ? » dépend de la propriété — les height en % sont un piège quand le parent n'a pas de hauteur définie.
remla police racine (16 px par défaut)Tailles de police et espacements qui suivent le texte. Toute l'échelle Tailwind est en rem sous le capot : p-4 génère padding: 1rem, pas 16 px.
emla police courantePadding d'un bouton proportionnel à son libellé. Rare, car les em se composent : un 1.2em dans un 1.2em fait 1,44 fois la base. En cas de doute, rem.
vh / vwla fenêtre visibleSections plein écran (min-height: 100vh). Avec modération sur mobile : la barre d'adresse apparaît et disparaît, ce qui rend « 100 % de la hauteur » flou.
La même propriété, cinq référentiels
width: 200px
width: 60%
width: 12rem
width: 12em
width: 25vw
Cinq barres, cinq façons de dire une largeur. 200px ne dépend de rien. 60% se mesure sur le conteneur. 12rem vaut douze fois la police racine, soit 192 px tant que l'utilisateur n'a rien réglé. 12em vaut douze fois la police de l'élément lui-même, ici plus petite : la barre est donc plus courte, alors que le nombre écrit est le même — c'est tout le piège des em. 25vw, enfin, ignore le conteneur et ne regarde que la fenêtre : c'est la seule qui change de longueur quand tu tournes ton téléphone.

Une seule ligne du tableau mérite un développement, parce que sa raison d'être est humaine et pas technique : rem. Écrire font-size: 1.25rem semble une façon compliquée de dire 20 px… jusqu'au jour où un utilisateur malvoyant règle la taille de police de son navigateur sur « grande ». Tous les rem de ta page grandissent alors proportionnellement, et la page reste lisible ; les tailles en px, elles, ignorent ce réglage — le texte reste petit et l'utilisateur est coincé. D'où une convention professionnelle sans ambiguïté : les tailles de police en rem, par respect pour le réglage de l'utilisateur. C'est un enjeu d'accessibilité, dans la lignée directe du module sur le HTML sémantique.

Les variables CSS : tes design tokens, version navigateur

Tu connais déjà le problème que les variables CSS résolvent, puisque Halterofit le résout tous les jours avec ses constantes de design : une couleur primaire écrite en dur à quarante endroits transforme un changement de charte en chasse au trésor, et une faute de frappe crée un quarante-et-unième bleu presque identique. La réponse est toujours la même — déclarer la valeur une fois, la référencer partout — et le CSS a son mécanisme natif : les custom properties, qu'on appelle couramment variables CSS.

La syntaxe tient en deux moitiés. Déclarer : une propriété dont le nom commence par deux tirets, --couleur-primaire: #2563eb;. Utiliser : la fonction var(). Par convention, les variables globales se déclarent sur :root — la racine du document — pour être visibles partout :

styles/theme.css
/* Le "fichier de tokens" de RendezVous : toutes les décisions visuelles, ici. */
:root {
  --couleur-primaire: #2563eb;   /* le bleu clinique */
  --couleur-fond: #ffffff;
  --couleur-texte: #1f2937;
  --rayon-carte: 12px;
  --espace-section: 48px;
}

/* Partout ailleurs, on RÉFÉRENCE, on n'écrit plus jamais la valeur. */
.bouton-rdv {
  background: var(--couleur-primaire);
  border-radius: var(--rayon-carte);
}
.service-card {
  background: var(--couleur-fond);
  color: var(--couleur-texte);
  border-radius: var(--rayon-carte);
}

Changer le bleu de la clinique = changer une ligne dans :root. Le reste du fichier n'y touche jamais.

Ce qui les rend puissantes — et différentes d'une simple constante — c'est qu'elles suivent la cascade. Une variable déclarée sur un élément vaut pour cet élément et tout son sous-arbre, et un descendant peut la redéfinir localement. D'où le cas d'usage roi, le thème :

styles/theme.css (suite)
/* Le mode sombre ne restyle RIEN : il redéfinit seulement les variables. */
.theme-sombre {
  --couleur-fond: #111827;
  --couleur-texte: #f9fafb;
  --couleur-primaire: #60a5fa;   /* un bleu plus clair, lisible sur fond sombre */
}
/* Aucune règle de .service-card, .bouton-rdv, etc. à réécrire :
   elles lisent var(--couleur-fond) et trouvent la nouvelle valeur.
   Basculer le thème = ajouter class="theme-sombre" sur <body>. Une ligne de JS. */

Le composant dit « je suis couleur de fond » ; le thème décide ce que « couleur de fond » veut dire aujourd'hui. Les deux ne se connaissent pas.

Note la mécanique, car c'est elle qu'on teste dans les questions pièges : quand le navigateur rencontre var(--couleur-fond), il remonte l'arbre depuis l'élément courant et prend la déclaration la plus proche. Une carte dans un <body class="theme-sombre"> trouve d'abord le #111827, jamais le blanc de :root. Ce mouvement — une valeur déposée en haut, lue par tout le sous-arbre, redéfinissable localement — est le même patron que le contexte React et ses Provider imbriqués, appliqué au style.

🔗 Pont — variables CSS ↔ design tokens de ton app

Dans Halterofit, les décisions visuelles vivent dans des constantes TypeScript (Colors.*, FONT_SIZE_*) et une règle ESLint maison interdit les valeurs en dur. Les variables CSS sont le même principe, autre syntaxe : Colors.primaryvar(--couleur-primaire), le dossier constants/ ↔ le bloc :root. L'idée commune s'appelle design tokens — un vocabulaire nommé de décisions visuelles, découplé des composants qui les consomment. Bonus côté CSS : la redéfinition en cascade offre le theming gratuitement, là où une constante TS a la même valeur partout.

✍️ Exercice de lecture

Un encart promotionnel dans la page d'accueil de RendezVous :

:root {
  --accent: #2563eb;
}
.encart-promo {
  --accent: #d97706;          /* orange, pour attirer l'œil */
}
.badge {
  background: var(--accent);
}
<body>
  <span class="badge">Nouveau</span>            <!-- badge A -->
  <div class="encart-promo">
    <span class="badge">-20 %</span>             <!-- badge B -->
  </div>
</body>

Questions : (1) De quelle couleur est le badge A ? (2) Le badge B ? (3) La règle .badge est pourtant identique pour les deux — qu'est-ce que ça montre sur la façon dont var() est résolu ?

Voir le corrigé

(1) Bleu (#2563eb). Le badge A n'est pas dans l'encart : en remontant l'arbre, la première déclaration de --accent qu'il trouve est celle de :root.

(2) Orange (#d97706). Le badge B est descendant de .encart-promo, dont la redéfinition de --accent est plus proche que celle de :root : elle gagne pour tout son sous-arbre.

(3) Que var() est résolu au moment de l'application, selon la position de l'élément dans l'arbre — pas au moment où la règle est écrite. Une même règle CSS produit des résultats différents selon le contexte d'héritage. C'est précisément le mécanisme qui rend le theming possible sans dupliquer les règles.

🔗 Pont — NativeWind, le même dictionnaire dans ton app

Une classe utilitaire n'est rien d'autre qu'une ou deux déclarations CSS avec un nom court — garde ce dictionnaire, il se lit dans les deux sens :

Classe Tailwind / NativeWindCSS généré
p-4padding: 1rem; (= 16 px)
px-4padding-left: 1rem; padding-right: 1rem;
flex-rowdisplay: flex; flex-direction: row;
items-centeralign-items: center;
justify-betweenjustify-content: space-between;
gap-2gap: 0.5rem;
grid grid-cols-3display: grid; grid-template-columns: repeat(3, 1fr);
rounded-mdborder-radius: 0.375rem;
bg-primarybackground-color: var(--primary); — une variable CSS, celles de la section précédente
md:flex-row@media (min-width: 768px) { ... } — le mobile-first de la section responsive

Dans Halterofit, NativeWind traduit ces mêmes classes non pas en CSS mais en styles natifs React Native — le module sur les composants et le style du guide Halterofit (Composants & style) montre la chaîne complète, de className à CVA. Un dictionnaire, deux cibles. D'où l'intérêt du vanilla pour toi : beaucoup de tests techniques fournissent un projet nu, sans Tailwind installé, parfois exprès — et pouvoir dire « je sais exactement quel CSS chaque classe génère » est la différence entre « je connais Tailwind » et « je connais le CSS, et Tailwind me fait aller plus vite ».

🧠 Quiz éclair

Une carte est à l'intérieur d'un élément qui redéfinit --accent: orange, alors que :root déclare --accent: bleu. Que rend var(--accent) dans la carte, et pourquoi ?

Orange : var() remonte l'arbre depuis l'élément courant et prend la déclaration la plus proche — la redéfinition locale masque celle de :root pour tout son sous-arbre. C'est ce mécanisme de cascade qui permet le theming (mode sombre) sans réécrire les règles des composants.

Position et empilement : le survol utile

Dernière famille d'outils, à connaître en lecture plus qu'en virtuosité : position, qui sort un élément du flux normal, et z-index, qui gère qui passe devant qui. Tu les croiseras dans tout code réel — badges, menus déroulants, en-têtes collants — et cinq valeurs suffisent pour lire sans trébucher.

  • static — le défaut : l'élément suit le flux normal, et top/left/z-index n'ont aucun effet sur lui. Retiens ce dernier point, il alimente le piège en bas de section.
  • relative — l'élément reste à sa place dans le flux, mais on peut le décaler (top: 4px…) et surtout il devient le point de repère de ses descendants en absolute. On le pose souvent sans aucun décalage, juste pour ce rôle d'ancre.
  • absolute — l'élément sort du flux (les autres font comme s'il n'existait pas) et se positionne par rapport à son ancêtre positionné le plus proche. Duo classique : parent relative, enfant absolute — la pastille de notification sur le coin d'une icône.
  • fixed — comme absolute, mais par rapport à la fenêtre : ne bouge pas au défilement. Bannières de cookies, boutons flottants.
  • sticky — l'hybride : l'élément défile normalement jusqu'à atteindre le seuil donné (top: 0), puis reste collé. C'est l'en-tête de RendezVous, visible pendant que le patient fait défiler les créneaux.
styles/badge.css
/* Le duo relative/absolute : la pastille "2" sur la cloche de notifications. */
.cloche {
  position: relative;      /* aucune autre propriété : la cloche sert juste d'ANCRE */
}
.cloche__pastille {
  position: absolute;      /* sortie du flux : elle flotte au-dessus */
  top: -4px;               /* positionnée par rapport à .cloche, l'ancêtre positionné */
  right: -4px;
}

/* L'en-tête collant de la page de prise de rendez-vous. */
.entete-slots {
  position: sticky;
  top: 0;                  /* défile, puis se colle en haut de l'écran */
  z-index: 10;             /* passe DEVANT les créneaux qui défilent dessous */
}

Les deux motifs à reconnaître au premier coup d'œil : l'ancre relative + enfant absolute, et le sticky; top: 0 d'un en-tête collant.

Un mot de plus sur sticky, parce que son échec est silencieux et qu'il coûte des heures. Un élément collant ne se cale pas par rapport à la fenêtre : il se cale par rapport à son conteneur de défilement, et n'importe quel ancêtre dont l'overflow vaut autre chose que visible — un overflow: hidden posé trois niveaux plus haut pour couper un débordement, par exemple — devient ce conteneur. L'élément colle alors à l'intérieur de cet ancêtre, qui bien souvent ne défile pas : à l'écran, il ne colle plus du tout. Ni erreur, ni avertissement. Deux autres conditions se vérifient dans la foulée : le top (ou bottom) est obligatoireposition: sticky tout seul ne fait rien —, et le parent direct doit être plus haut que l'élément, faute de quoi il n'y a aucune distance sur laquelle coller. Quand un en-tête collant refuse de coller, remonte la chaîne des parents à la recherche d'un overflow avant toute autre chose.

static, relative, absolute — dans une même ancre
1 · static
2 · relative, left: 26px
3 · static
absolute
Le cadre en pointillé est l'ancre : il porte position: relative sans aucun décalage, uniquement pour servir de repère. Les blocs 1 et 3 sont en static et suivent le flux. Le bloc 2 est en relative, décalé de 26 px vers la droite — et c'est là qu'il faut regarder le bloc 3 : il n'a pas bougé d'un pixel. La place d'origine du bloc 2 lui reste réservée, le décalage n'est qu'un déplacement visuel. La quatrième boîte, elle, est en absolute : sortie du flux, elle s'est placée dans le coin de l'ancre, et les trois autres se comportent comme si elle n'existait pas. fixed ferait la même chose par rapport à la fenêtre, et sticky ne se distingue qu'en défilant — ces deux-là ne se montrent pas sur une image fixe.

Reste z-index : quand deux éléments positionnés se chevauchent, il décide lequel est dessiné devant — plus le nombre est grand, plus l'élément est « proche de toi ». La règle capitale, celle que tout le monde apprend à ses dépens : z-index n'a d'effet que sur un élément positionné (ou dans quelques autres contextes, comme les enfants d'une grille ou d'un flex). Sur un élément resté en static — le défaut ! — il est purement ignoré.

🧠 Quiz éclair

Ton z-index: 999 ne change rien. Premier réflexe ?

Vérifier la position de l'élément : sur un élément en static (le défaut), z-index est ignoré silencieusement. Ajouter position: relative; suffit le plus souvent à l'« activer ». Et si ça ne suffit toujours pas, le mot-clé à creuser est stacking context.

⚠️ Piège fréquent

Le grand classique : « mon z-index: 999 ne marche pas ! ». Neuf fois sur dix, l'élément est en position: static — la valeur par défaut — et le navigateur ignore silencieusement le z-index : ni erreur, ni avertissement, juste rien. Le remède tient en une ligne : ajouter position: relative; (sans décalage, ça ne bouge rien visuellement) pour « activer » le z-index. Avant d'empiler des 999, des 9999 et des 99999 en désespoir de cause, vérifie toujours la position de l'élément.

🧭 Bon à savoir (et honnêteté due)

Il existe derrière z-index une mécanique plus profonde, les stacking contexts (contextes d'empilement) : certains éléments créent une « bulle » d'empilement locale, et un z-index: 9999 à l'intérieur d'une bulle ne pourra jamais passer devant un élément situé hors de la bulle, quel que soit le nombre. C'est la cause des bugs de superposition vraiment tenaces (le menu qui passe sous le modal…). On ne plonge pas dedans ici : sache simplement que ce puits existe, que « monter le z-index » n'est pas toujours la réponse, et que « stacking context » est le mot-clé à chercher le jour où tu tomberas dedans.

z-index : ce qu'on attend, et le puits
trois frères — le plus grand passe devant
z-index: 1
z-index: 2
z-index: 3
le 9999 enfermé dans une bulle
z-index: 9999
z-index: 2
En haut, le cas attendu : trois frères positionnés, et le plus grand nombre passe devant. En bas, le puits dont parle l'aparté ci-dessus. La carte enfermée dans la bulle porte z-index: 9999, sa voisine restée dehors seulement 2 — et pourtant c'est la voisine qui passe devant. Le parent de la première porte un opacity: 0.99, ce qui suffit à créer un contexte d'empilement : le 9999 ne se compare plus qu'aux éléments de sa bulle, et c'est la bulle entière, avec son z-index: 1, qui se compare au reste. Aucun nombre, si grand soit-il, ne fait sortir un élément de sa bulle.

Transitions : le minimum vital pour lire du code réel

Un mot, pour finir, sur une propriété que tu verras dans à peu près chaque feuille de style réelle : transition. Par défaut, un changement de style est instantané — un bouton qui passe de bleu à bleu foncé au survol saute d'une couleur à l'autre. transition demande au navigateur d'animer ce changement : tu déclares quelle(s) propriété(s) surveiller et en combien de temps lisser le passage.

.bouton-rdv {
  background: var(--couleur-primaire);
  transition: background 200ms;   /* propriété à animer + durée */
}
.bouton-rdv:hover {
  background: #1d4ed8;            /* le survol change le fond… */
}
/* …et la transition lisse le passage sur 200 ms, aller ET retour. */

La transition se déclare sur l'état de base, pas sur le :hover : ainsi elle joue dans les deux sens, à l'entrée comme à la sortie du survol.

C'est tout ce qu'il faut pour lire du code : transition: propriété durée, parfois complété d'une courbe d'accélération (ease-out…) et d'un délai. Deux repères de bon goût : des durées courtes (150–300 ms, au-delà l'interface paraît molle), et animer opacity et transform, que le navigateur sait accélérer, plutôt que des propriétés de mise en page comme width. L'animation sérieuse aura son propre guide dans l'Atlas (GSAP, à venir) ; ici on s'arrête au niveau « je comprends ce que fait cette ligne ».

🎤 En entrevue

« Flexbox ou Grid : comment choisis-tu entre les deux ? »

« Je me demande qui doit dicter la disposition. Si c'est le contenu — des éléments de tailles naturelles différentes à aligner le long d'un axe, comme une barre de navigation — je prends flexbox : une dimension, les enfants gardent l'initiative. Si c'est la structure — je sais d'avance que je veux trois colonnes ou un quadrillage de cartes — je prends grid : deux dimensions, le parent déclare la grille et les enfants s'y rangent. Et les deux se combinent : typiquement une grille de cartes en grid, avec du flex à l'intérieur de chaque carte. Mon signal d'alarme, c'est quand je calcule des largeurs en pourcentage sur des enfants flex pour simuler des colonnes — c'est que j'aurais dû prendre grid. »

« C'est quoi, le mobile-first ? Pourquoi cette approche a-t-elle gagné ? »

« C'est écrire le CSS de base pour le petit écran — la version la plus simple, une colonne — puis ajouter des media queries en min-width qui enrichissent la mise en page quand la place augmente. L'inverse, desktop-first, part du grand écran et retire des choses en max-width. Mobile-first a gagné pour trois raisons : le style par défaut est le plus robuste, celui qui s'applique si rien d'autre ne matche ; ça force à hiérarchiser le contenu essentiel au lieu de compresser une page chargée ; et l'outillage moderne l'a entériné — les préfixes responsive de Tailwind, comme md:, sont des min-width, donc du mobile-first par construction. »

« Tailles en px ou en rem ? »

« Pour les tailles de police, rem, et c'est un enjeu d'accessibilité : le rem est relatif à la taille de police racine, donc si l'utilisateur agrandit la police dans les réglages de son navigateur, tout suit proportionnellement — alors que des tailles en px ignorent ce réglage et laissent le texte petit. Pour le purement géométrique — bordures de 1 px, arrondis, taille d'une icône — le px reste légitime : ces valeurs n'ont pas de raison de grandir avec le texte. Donc : pas de dogme, mais la typo en rem, toujours. D'ailleurs l'échelle de Tailwind est entièrement en rem sous le capot. »

À retenir

Grid pose une grille en deux dimensions depuis le parent (display: grid + grid-template-columns, l'unité fr = part de l'espace libre) ; flex reste l'outil des alignements sur un axe — le contenu dicte → flex, la grille dicte → grid, et les deux s'imbriquent. La formule du module, repeat(auto-fit, minmax(240px, 1fr)), donne une grille responsive sans media query. Le responsive repose sur la balise viewport + les media queries, écrites mobile-first (base simple, paliers en min-width). Unités : la typo en rem (accessibilité), le géométrique en px, vh/vw pour le plein écran. Les variables CSS (--nom / var(--nom)) sont tes design tokens version navigateur, avec la cascade en bonus — d'où le theming en une classe. Une classe Tailwind n'est que du CSS nommé : savoir les deux est un atout d'entrevue. Et z-index exige un élément positionné.

Et ailleurs : Presque tout ce module est un principe général déguisé en CSS. « Déclarer la contrainte, laisser le moteur calculer » (la grille qui respire) est l'essence du style déclaratif — la philosophie du JSX. Les design tokens (un nom, une valeur, un seul endroit) se retrouvent dans constants/colors.ts, dans le :root d'un site, dans le thème d'une lib de composants : change d'outil, garde le réflexe. « La déclaration la plus proche dans l'arbre gagne » est le patron du contexte React. Et savoir traduire une abstraction vers ce qu'elle génère — Tailwind → CSS, demain ORM → SQL — est la compétence qui distingue celui qui comprend ses outils de celui qui les subit.

🗂️ L'aide-mémoire
fr
une part de l'espace qui reste une fois les largeurs fixes prélevées. 2fr 1fr donne donc deux tiers, puis un tiers
auto-fit ou auto-fill
auto-fit effondre les colonnes vides, et les cartes s'étirent pour remplir la ligne. auto-fill les garde, et les cartes restent à leur largeur minimale
Ce qu'un conteneur grid ou flex commande
ses enfants directs, et eux seuls. Un wrapper glissé entre le conteneur et ses enfants coupe la relation : les petits-enfants ne voient plus la grille
Le contenu de la balise viewport
width=device-width et initial-scale=1. Sans cette balise, le navigateur mobile fait semblant d'être large et aucune media query mobile ne s'applique
L'ordre des paliers min-width
par largeurs croissantes. Sur un grand écran les paliers s'appliquent tous en même temps, et comme leurs sélecteurs pèsent pareil, c'est le dernier écrit qui gagne : le plus large doit donc finir
Le référentiel de rem et em
rem se mesure sur la police racine (16 px par défaut). em se mesure sur celle de l'élément courant, et se multiplie donc en s'imbriquant : un 1.2em dans un 1.2em vaut 1,44 fois la base
La valeur par défaut de position
static. Sur un élément resté static, top, left et z-index sont ignorés en silence
Le repère d'un absolute
l'ancêtre positionné le plus proche, c'est-à-dire dont la position n'est pas static. D'où l'habitude de poser position: relative sur le parent voulu, sans aucun décalage, juste pour en faire l'ancre
Ce qui neutralise un sticky
l'absence de top, qui dit à partir d'où coller ; ou un ancêtre dont l'overflow n'est pas visible, qui capte le collage. En silence dans les deux cas
Où se déclare une transition
sur l'état de base, jamais sur le :hover. Déclarée sur le :hover, elle anime l'entrée du survol mais pas la sortie, qui redevient brutale