Module 3 · Fondations du web

CSS 1 — modèle de boîte & flexbox

Tu écris du style tous les jours avec NativeWind — mais derrière chaque classe p-4 ou flex-row se cache du CSS, le vrai. Les tests techniques de stage exigent très souvent du CSS vanilla, sans Tailwind ni framework : il faut savoir lire une feuille de style, expliquer le modèle de boîte, et centrer une div sans chercher sur Google. Ce module pose ces fondations en s'appuyant en permanence sur ce que tu connais déjà côté React Native.

💡 Les deux grandes idées du module
  • Tout est boîte. Chaque élément d'une page web est un rectangle avec un contenu, une marge intérieure (padding), une bordure et une marge extérieure (margin). Une fois qu'on voit les boîtes, on voit la page.
  • Flexbox range les boîtes. C'est le même flexbox que tu utilises en React Native — à deux ou trois défauts près, et ces défauts sont précisément là où les devs mobiles se font piéger en passant au web. On va les nommer un par un.

Ce que « cascade » veut dire : plusieurs règles se disputent un élément

CSS signifie Cascading Style Sheets, et le mot qui fait la personnalité du langage est cascade. Le problème qu'elle résout : sur une page réelle, un même élément est presque toujours visé par plusieurs règles. Le style par défaut du navigateur dit que les liens sont bleus et soulignés, ta feuille dit qu'ils sont verts, une règle plus bas dit que les liens dans le menu sont blancs. Trois règles, un lien : qui gagne ? La cascade est l'algorithme qui tranche, et il tranche toujours, de façon déterministe. Quand un style « ne marche pas », il n'y a pas de mystère — une autre règle a gagné la dispute, et on peut trouver laquelle.

L'arbitrage se joue en trois temps. Un — l'origine : les styles par défaut du navigateur (c'est pour ça qu'un <h1> est gros et gras sans qu'on l'ait demandé) passent derrière ceux que toi tu écris. Deux — la spécificité : entre deux de tes règles, la plus « précise » gagne. Trois — l'ordre d'apparition : à spécificité égale, la règle écrite en dernier l'emporte. Ce dernier point devrait te rappeler quelque chose : c'est la règle « la dernière classe gagne » de tailwind-merge, vue dans le guide Halterofit — twMerge imite en JavaScript ce que la cascade fait nativement.

La spécificité, maintenant. On la présente souvent avec une arithmétique obscure de points ; la version simple suffit pour lire du CSS et répondre en entrevue. Quatre niveaux de poids, du plus lourd au plus léger :

  1. Le style inline — écrit directement sur l'élément, <div style="color: red">. Il bat tout le reste (hors cas exotiques).
  2. L'id — le sélecteur #header. Un id est unique dans la page, donc une règle sur un id est très ciblée : elle pèse lourd.
  3. La classe — le sélecteur .card. Moins ciblée qu'un id (une classe peut être posée sur cent éléments), donc moins lourde.
  4. L'élément — le sélecteur p ou a, qui vise tous les paragraphes ou tous les liens. Le plus générique, donc le plus léger.

L'intuition à garder : plus un sélecteur est ciblé, plus il gagne, et un sélecteur composé cumule — .menu .lien (deux classes) bat .lien (une classe). C'est tout ce qu'il faut pour lire 99 % des feuilles de style réelles.

Dernière chose, et elle explique beaucoup de CSS moderne : les développeurs s'en tiennent presque exclusivement aux classes. Les ids et le style inline sont des armes trop lourdes — une fois qu'une règle #header existe, la surcharger demande un sélecteur encore plus lourd, puis un autre, et la feuille devient une course à l'armement (avec au bout le tristement célèbre !important, qui force une règle à gagner et rend le fichier ingérable). En restant sur des classes, tout le monde joue dans la même catégorie de poids et c'est l'ordre d'écriture, simple et prévisible, qui départage. Tailwind pousse la logique à l'extrême : que des classes, jamais d'id, jamais d'inline.

🧭 Bon à savoir

En React Native, tu n'as jamais eu à penser à tout ça : le style d'un composant est un objet qui s'applique à ce composant, point — pas de cascade, pas de spécificité, pas de règle qui « fuit ». C'est justement pour retrouver ce confort que le web a inventé l'« utility-first » et les composants : confiner le style pour ne plus subir la cascade globale. Comprendre la cascade, c'est comprendre pourquoi Tailwind existe.

🧠 Quiz éclair

Le titre de la carte s'obstine à rester gris, alors que la seconde règle est bien plus détaillée et écrite plus bas dans le fichier. De quelle couleur est-il, et qu'est-ce que ça t'apprend sur la spécificité ?

#promo .carte .titre { color: gray; }

.encart .carte .titre.actif { color: #2563eb; }

Gris : c'est la première règle qui gagne. La spécificité ne s'additionne pas d'un niveau de poids à l'autre — on compare d'abord le nombre d'ids, et seulement en cas d'égalité le nombre de classes, puis d'éléments. Un seul #promo écrase donc quatre classes, quel que soit leur nombre, et l'ordre d'écriture ne départage qu'à spécificité strictement égale.

C'est exactement la course à l'armement décrite plus haut : dès qu'un id entre dans une feuille de style, seul un autre id — ou un !important — peut le surcharger, et on s'enferme. D'où la discipline moderne, que Tailwind pousse à l'extrême : que des classes, tout le monde dans la même catégorie de poids, et c'est l'ordre d'écriture, simple et lisible, qui tranche.

Les sélecteurs essentiels, en lecture

Une règle CSS a toujours la même anatomie : un sélecteur (qui est visé ?), puis un bloc de déclarations entre accolades, chacune une paire propriété: valeur;. L'objectif ici n'est pas de réciter tous les sélecteurs — il y en a des dizaines — mais de savoir lire ceux qui couvrent l'immense majorité des feuilles réelles :

Les sélecteurs qu'on croise partout
/* Élément : « tous les paragraphes » */
p { line-height: 1.6; }

/* Classe : « tout élément portant class="card" » — LE sélecteur du quotidien */
.card { border-radius: 8px; }

/* Id : « L'élément portant id="header" » — rare dans le CSS moderne */
#header { position: sticky; }

/* Descendant (un ESPACE) : « les .prix qui sont QUELQUE PART dans une .card,
   enfant, petit-enfant, peu importe la profondeur » */
.card .prix { font-weight: 700; }

/* Enfant direct (un CHEVRON) : « les li directement enfants du .menu,
   pas les li imbriqués plus profond » */
.menu > li { padding: 8px; }

/* États : au survol de la souris, au focus clavier */
.bouton:hover { background: #0f766e; }
.champ:focus { outline: 2px solid #0f766e; }

Six formes de sélecteurs. Si tu sais lire ça, tu sais lire l'essentiel de n'importe quelle feuille de style.

Trois précisions de lecture. D'abord l'espace contre le chevron : a b (descendant) ratisse large — n'importe quel b à n'importe quelle profondeur sous un a — tandis que a > b (enfant direct) ne vise que le premier niveau. Quand un style semble « traverser » toute une sous-arborescence, cherche un sélecteur descendant.

Ensuite les pseudo-classes comme :hover et :focus : le deux-points signale un état, pas un élément différent. .bouton:hover se lit « le .bouton, quand la souris est dessus » ; :focus, « quand il a le focus clavier » — tu as vu son importance pour l'accessibilité au module précédent. Note qu'un état de survol n'existe pas en mobile natif : il n'y a pas de souris.

Enfin les sélecteurs à double deux-points, ::before et ::after : des pseudo-éléments, un contenu fantôme que CSS insère avant ou après le contenu réel, typiquement pour une décoration (puce maison, trait, icône) sans ajouter de balise. En lecture, .lien::before { content: '→'; } se comprend « CSS dessine une flèche juste avant le texte du lien ». Tu n'as pas besoin d'en écrire, juste de ne pas être dérouté en en voyant.

✍️ Exercice de lecture

Voici un extrait de feuille de style. Trois questions en dessous.

a { color: #2563eb; }
.menu a { color: white; text-decoration: none; }
.menu > li:hover { background: #1e293b; }

Questions : (1) De quelle couleur est un lien posé dans le corps de la page, hors du menu ? (2) De quelle couleur est un lien dans le menu, et pourquoi la première règle ne gagne-t-elle pas ? (3) Que se passe-t-il quand la souris survole un élément de liste directement enfant du menu ?

Voir le corrigé

(1) Bleu (#2563eb) : seule la règle a le vise.

(2) Blanc, et sans soulignement. Les deux règles visent le même lien, mais .menu a (une classe + un élément) est plus spécifique que a tout seul (un élément) : elle gagne la dispute. C'est la cascade en action — spécificité d'abord, ordre ensuite.

(3) Son fond devient gris-bleu foncé (#1e293b) tant que la souris est dessus. Le > précise : seulement les li de premier niveau du menu, pas ceux d'un éventuel sous-menu imbriqué. Le :hover précise : seulement pendant le survol.

Le modèle de boîte : content, padding, border, margin

Question d'entrevue numéro un du CSS, et à juste titre : c'est la fondation de tout le reste. Chaque élément d'une page — chaque paragraphe, chaque image, chaque div — est rendu comme un rectangle, et ce rectangle a quatre couches concentriques, de l'intérieur vers l'extérieur :

  1. Le contenu (content) : le texte, l'image, ce qu'il y a dedans.
  2. Le padding : la marge intérieure, l'espace entre le contenu et la bordure. Il porte la couleur de fond de l'élément — un fond coloré s'étend jusque sous le padding.
  3. La bordure (border) : le trait qui entoure, avec une épaisseur, un style et une couleur.
  4. La margin : la marge extérieure, l'espace entre cette boîte et ses voisines. Elle est toujours transparente — c'est du vide entre les boîtes.

L'analogie classique est le tableau encadré : le contenu est la toile, le padding le passe-partout, la bordure le cadre, la margin l'espace entre deux tableaux sur le mur. Cette distinction te sert déjà en React Native — même modèle, mêmes noms.

Les quatre couches, de l'extérieur vers l'intérieur
margin
border
padding
contenu
Chaque couche est ici un vrai élément dont le padding est la couche montrée : l'épaisseur que tu vois est celle que le navigateur applique. De l'extérieur vers l'intérieur : la margin (ambre, en pointillé — elle est transparente, c'est du vide entre les boîtes), la bordure (rouge), le padding (vert — il porte le fond de l'élément), puis le contenu (bleu).

Le piège historique que RN n'a pas tient à une question : « quand j'écris width: 180px, qu'est-ce qui mesure 180 pixels ? » Le comportement d'origine, content-box, répond le contenu seul : padding et bordure s'ajoutent par-dessus. Une carte à width: 180px avec padding: 18px et une bordure de 3 px occupe en réalité 180 + 18 + 18 + 3 + 3 = 222 pixels. Personne ne raisonne comme ça : quand tu dis « cette carte fait 180 px », tu penses au rectangle visible. Ce décalage a causé des années de mises en page cassées au pixel près.

La même déclaration, deux façons de la mesurer
box-sizing: content-box
width: 180px
occupe 222 px
box-sizing: border-box
width: 180px
occupe 180 px
Les deux boîtes reçoivent exactement les mêmes déclarations — width: 180px, padding: 18px, bordure de 3 px — et ne diffèrent que par box-sizing. La règle posée sous chaque boîte mesure la largeur réellement occupée : 222 px en content-box, 180 en border-box. En border-box, c'est le contenu qui se comprime pour laisser la place.

La solution universellement adoptée : box-sizing: border-box. Avec elle, width: 180px mesure le rectangle visible, bordure comprise — le contenu se comprime pour laisser la place au padding et à la bordure à l'intérieur. C'est le comportement intuitif, d'où le « reset » qui ouvre pratiquement tous les projets web (quelques règles en tête de feuille pour neutraliser des défauts gênants du navigateur) :

/* Le reset le plus répandu du web : TOUT passe en border-box.
   L'étoile * sélectionne tous les éléments. */
*, *::before, *::after {
  box-sizing: border-box;
}

Trois lignes présentes au sommet de presque toutes les feuilles de style du monde. Quand tu lis un CSS de test technique, pars du principe qu'elles y sont — et vérifie.

🔗 Pont — du StyleSheet RN au CSS réel

En React Native, box-sizing n'existe pas, et pour cause : Yoga, le moteur de layout de RN, se comporte déjà comme border-box. Ton intuition est donc la bonne — il te suffit de savoir que le web, lui, doit la demander explicitement. Plus largement, chaque classe NativeWind que tu tapes est la traduction d'une déclaration CSS de ce module : p-4 = padding: 16px, border = border-width: 1px. Le module sur les composants et le style du guide Halterofit (composants.html) montrait comment ces classes s'assemblent ; ici tu découvres ce qu'elles veulent dire en dessous.

🧭 Aparté honnête : les marges qui fusionnent

Le web a une bizarrerie que RN n'a pas : le margin collapsing. Quand deux blocs se suivent verticalement, leurs marges qui se touchent ne s'additionnent pas — elles fusionnent, et seule la plus grande compte : margin-bottom: 24px suivi de margin-top: 16px donne 24 px, pas 40. C'était pensé pour la typographie, mais c'est déroutant la première fois qu'un espacement « disparaît ». Deux garde-fous : ça ne concerne que les marges verticales de blocs dans le flux normal (la mise en page par défaut, tant qu'aucun conteneur n'a demandé flexbox), et ça n'arrive jamais dans un conteneur flex. Sache que ça existe pour ne pas perdre vingt minutes le jour où ça te tombe dessus.

Deux marges qui se touchent : 32 px, pas 48
flux normal
.intro
.services
écart : 32 px
display: flex
.intro
.services
écart : 48 px
Les deux piles contiennent les mêmes blocs, avec exactement les mêmes marges — margin-bottom: 32px sur le premier, margin-top: 16px sur le second. Seul le conteneur change, et les deux départs sont alignés pour que l'écart se compare d'un coup d'œil. À gauche, dans le flux normal, les deux marges fusionnent et seule la plus grande compte : 32 px. À droite, en display: flex, elles s'additionnent : 48 px. Deux modes de mise en page, deux arithmétiques.
🧠 Quiz éclair

Deux blocs se suivent verticalement dans la page. Quel espace les sépare réellement ? Et si leur parent recevait display: flex; flex-direction: column ?

.intro    { margin-bottom: 32px; }
.services { margin-top: 16px; }

32 px, pas 48. Dans le flux normal, deux marges verticales qui se touchent ne s'additionnent pas : elles fusionnent, et seule la plus grande compte. Le margin-top: 16px est intégralement avalé — il ne sert à rien tant que l'autre marge est plus grande, ce qui est la cause numéro un des espacements « qui ne bougent pas quand on les change ».

Avec un parent en display: flex : 48 px. La fusion n'existe que dans le flux normal ; dès qu'un conteneur passe en flex, ses enfants ne sont plus des blocs de ce flux et leurs marges s'additionnent comme ton intuition de React Native l'attend. Deux modes de mise en page, deux arithmétiques — d'où l'intérêt, en lisant du CSS, de repérer d'abord dans quel mode on se trouve.

display : block, inline, et le flux normal du document

Voici, conceptuellement, la plus grosse différence entre le web et React Native. En RN, aucune question à se poser : toute View est positionnée par flexbox, toujours. Sur le web, avant même que flexbox n'entre en scène, les éléments sont posés par le flux normal du document — l'algorithme de mise en page par défaut, hérité de l'époque où une page web était littéralement une page de texte. Le comportement de chaque élément dans ce flux dépend de sa propriété display.

display: block : l'élément se comporte comme un paragraphe — il prend toute la largeur disponible et force un retour à la ligne avant et après, si bien que les blocs s'empilent verticalement. <div>, <p>, <h1>, <section>, <ul> sont des blocs, et acceptent width, height et marges verticales.

display: inline : l'élément se comporte comme un mot dans une phrase. Il se glisse dans le fil du texte, ne provoque aucun retour à la ligne, et — piège classique — ignore width, height et les marges verticales : on ne donne pas une hauteur à un mot au milieu d'une phrase. <a>, <span>, <strong>, <code> sont inline par défaut.

display: inline-block : l'hybride — dans le fil du texte comme un mot, mais acceptant largeur, hauteur et marges. Très utilisé pour les boutons et menus horizontaux avant flexbox ; aujourd'hui surtout croisé dans du vieux code. Et display: none : l'élément disparaît complètement, n'occupe plus aucune place, comme s'il n'était pas dans le HTML — à distinguer de visibility: hidden (invisible mais l'espace reste réservé) et, côté React, du rendu conditionnel condition && <Composant /> qui retire carrément l'élément de l'arbre.

Pourquoi cette taxonomie alors que « tout le monde utilise flexbox » ? D'abord parce que le flux normal reste le défaut : tant que personne n'a écrit display: flex, c'est lui qui commande, et une page de contenu textuel se met très bien en page avec lui seul. Ensuite parce qu'il explique des comportements qui semblent aléatoires quand on vient du mobile : « pourquoi ma div prend toute la largeur ? » (c'est un bloc), « pourquoi mon width est ignoré ? » (c'est un inline), « pourquoi ces images ont un espace mystérieux entre elles ? » (elles sont inline, donc traitées comme des mots séparés par des espaces). Le dev qui connaît display lit ces situations en une seconde ; l'autre ajoute des règles au hasard.

Le même badge, trois valeurs de display
display: inline — width et height IGNORÉS

Du texte avec un badge au milieu de la phrase.

display: inline-block — les deux sont honorés

Du texte avec un badge au milieu de la phrase.

display: block — il prend sa propre ligne

Du texte avec un badge au milieu de la phrase.

Les trois badges portent exactement les mêmes déclarations — width: 130px et height: 38px — et seul display change. Le premier les ignore et se contente de la place de son texte : c'est le piège du « pourquoi mon width ne fait rien ». Le deuxième les respecte tout en restant dans le fil de la phrase. Le troisième force un retour à la ligne avant et après.
💡 Le renversement mental à opérer

En React Native : flexbox partout, par défaut, sans opt-in. Sur le web : flux normal par défaut, flexbox sur demande — chaque conteneur qui veut ranger ses enfants au flexbox doit le déclarer avec display: flex. Quand tu lis du CSS, repère ces display: flex : ce sont les endroits où la mise en page « passe en mode RN ». Tout le reste vit dans le flux normal.

🧠 Quiz éclair

Le badge refuse de faire 80 px de large, sa hauteur est ignorée et l'espace au-dessus n'apparaît jamais — alors que les DevTools montrent bien la règle appliquée, sans rien de barré. Que se passe-t-il ?

<p>Consultation générale <span class="badge">Populaire</span></p>
.badge {
  width: 80px;
  height: 24px;
  margin-top: 12px;
  padding: 0 8px;
  background: #fde68a;
}

Un <span> est inline par défaut : le navigateur le traite comme un mot au milieu d'une phrase. Et un mot n'a ni largeur imposée, ni hauteur, ni marge verticale — width, height, margin-top et margin-bottom sont donc purement ignorés. Pas d'erreur, pas d'avertissement, et la règle apparaît bien dans les DevTools : c'est ce qui rend le symptôme si déroutant quand on vient de React Native, où toute View est une boîte.

Ce qui marche quand même, et qui trompe : le padding horizontal et le background. D'où l'impression que « la règle est prise en compte à moitié ». Le correctif tient en une déclaration : display: inline-block pour rester dans le fil du texte tout en devenant une vraie boîte, ou display: flex/block si le badge doit vivre sur sa propre ligne.

Flexbox : le conteneur range, les enfants négocient

On arrive en terrain connu : chaque flex-row, chaque items-center, chaque justify-between de NativeWind, c'est du flexbox. Le principe, pour le redire proprement une fois : on pose display: flex sur un conteneur, et ses enfants directs deviennent des « items flexibles » rangés le long d'un axe. Les propriétés se répartissent en deux familles selon où on les écrit — c'est la seule chose à ne jamais confondre, et le tableau te la donne d'un coup d'œil.

Sur le CONTENEUR (celui qui porte display: flex)Ce qu'elle décide
flex-direction L'axe principal : row (défaut web) ou column, plus les variantes -reverse.
justify-content Distribue les enfants le long de l'axe principal : flex-start (défaut), center, flex-end, space-between (premier et dernier aux bords — le patron des barres d'en-tête), space-around, space-evenly.
align-items Aligne sur l'axe croisé, perpendiculaire : stretch (défaut des deux côtés), center, flex-start, flex-end, baseline.
flex-wrap nowrap (défaut) garde tout sur une ligne quitte à comprimer ; wrap laisse passer à la ligne — la base des grilles de cartes qui s'adaptent à l'écran.
gap Un espace entre les enfants seulement (ni avant le premier, ni après le dernier). Avant elle, on posait des marges qu'il fallait annuler sur le dernier.
Sur un ENFANTCe qu'elle décide
flex-grow « Si le conteneur a de l'espace en trop, quelle part j'en prends ? » — 0 (défaut) : rien. Les valeurs sont des proportions : un enfant à 2 prend deux fois plus qu'un enfant à 1.
flex-shrink « Si l'espace manque, j'accepte de rétrécir ? » — 1 (défaut web) : oui ; 0 : non, et ça déborde.
flex-basis « Ma taille de départ, avant que grow et shrink n'ajustent. » Défaut auto = ma taille de contenu, ou mon width si j'en ai un.
flex: 1 Le raccourci roi : grow: 1; shrink: 1; basis: 0. Comme la base est zéro, tout l'espace devient « libre à répartir » — d'où des enfants tous à flex: 1 exactement de la même taille, quel que soit leur contenu. C'est le flex-1 de NativeWind.
align-self L'exception individuelle : cet enfant-là déroge à l'align-items du conteneur, sans toucher à ses frères.
justify-content — les six valeurs, mêmes enfants
flex-start (défaut)
1
2
3
center
1
2
3
flex-end
1
2
3
space-between
1
2
3
space-around
1
2
3
space-evenly
1
2
3
Six conteneurs identiques, trois enfants identiques : seule la valeur de justify-content change. space-between colle le premier et le dernier aux bords — c'est le patron des barres d'en-tête. space-around donne à chaque enfant la même marge de part et d'autre, si bien que l'espace des extrémités paraît deux fois plus petit que celui du milieu. space-evenly égalise vraiment tous les intervalles. Ces trois-là sont indistinguables en mots. Côte à côte, plus jamais.

Le moyen mnémotechnique qui sert depuis quinze ans : justify = l'axe principal, align = l'axe croisé. En row, justify gère l'horizontal et align le vertical ; en column, c'est l'inverse. Tu connais déjà cette gymnastique par NativeWind, elle est identique.

align-items — l'axe croisé, sur des enfants inégaux
stretch (défaut) — tous à la hauteur du conteneur
1
2
3
flex-start
1
2
3
center
1
2
3
flex-end
1
2
3
Les trois enfants ont des hauteurs différentes, et le conteneur est plus haut qu'eux — sans ces deux conditions, l'axe croisé n'a rien à aligner, et c'est exactement pourquoi align-items semble souvent « ne rien faire ». stretch, le défaut, étire tout le monde à la hauteur du conteneur et efface les différences ; les trois autres laissent chaque enfant à sa taille et le placent en haut, au milieu ou en bas. La cinquième valeur, baseline, aligne la ligne d'écriture des textes plutôt que le bord des boîtes : utile quand les polices diffèrent, rarement décisive ailleurs.

Trois choses méritent en revanche plus qu'une ligne de tableau, parce que ce sont elles qui te feront trébucher. La première : les défauts opposés. Le défaut web de flex-direction est row, celui de React Native est column — même moteur, choix inverses. Le web a choisi row parce que flexbox y a d'abord servi à faire des barres de navigation ; RN a choisi column parce qu'un écran de téléphone est une colonne. Conséquence concrète : le réflexe d'écrire flex-row pour mettre en ligne, oublie-le en CSS vanilla — display: flex tout seul met déjà en ligne. C'est la colonne qu'il faut demander.

La deuxième : flex-shrink. Sur le web il vaut 1, donc un enfant accepte de rétrécir quand la place manque ; en React Native il vaut 0, donc l'enfant déborde plutôt que de céder. Même code, deux rendus — et c'est presque toujours la cause quand un layout se comporte « bizarrement » en passant d'une plateforme à l'autre. La troisième, min-width: 0, a droit à son aparté un peu plus bas : c'est le plancher caché qui empêche flex-shrink de faire son travail.

⚠️ Piège fréquent — le layout « à l'envers »

Le symptôme est toujours le même : tu portes un écran de RN vers le web, et tout se met en ligne alors que tu attendais une colonne — ou l'inverse, un enfant déborde là où il rétrécissait sagement. Ne relis pas ton HTML : ce sont les deux défauts qu'on vient de voir, et il n'y en a jamais un troisième. Vérifie flex-direction, puis flex-shrink, dans cet ordre. Le réflexe à installer : à chaque display: flex écrit ou lu, une demi-seconde pour te demander « axe principal : lequel ? ».

Un mot d'expérience vécue, tant qu'on est sur flex: 1 : tu as déjà rencontré côté mobile le gotcha du flex-1 qui « s'effondre » — un enfant mesuré à zéro dans un parent sans hauteur contrainte. Le principe vaut des deux côtés : flex: 1 distribue l'espace disponible ; si le parent n'a pas de taille définie sur cet axe, il n'y a rien à distribuer. Quand un flex: 1 ne marche pas, remonte d'un cran : c'est presque toujours le parent.

Visualisons avec l'app fil rouge. La page d'accueil de RendezVous montre trois cartes de services — « Consultation », « Vaccination », « Prise de sang » — alignées en une rangée régulière, chacune de même hauteur, avec un espace constant entre elles. En CSS vanilla, tout tient en une règle sur le conteneur :

styles.css — la rangée de services de RendezVous
.services {
  display: flex;        /* les enfants directs passent en mode flex        */
  /* pas de flex-direction : le défaut web est ROW → déjà en rangée        */
  gap: 16px;            /* 16px entre chaque carte, ni avant ni après      */
  align-items: stretch; /* toutes les cartes prennent la hauteur de la     */
}                       /* plus grande — c'est le défaut, écrit pour lire  */

.services .carte {
  padding: 16px;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
}

Une rangée de cartes : quatre lignes sur le conteneur. Remarque ce qu'on n'a PAS écrit : aucun flex-direction (row est le défaut) et aucune marge sur les cartes (gap s'en charge).

🔗 Pont — flexbox RN ↔ flexbox web, la table des défauts

C'est le même algorithme (RN embarque Yoga, une réimplémentation de la spec flexbox), mais les valeurs par défaut diffèrent — et ces différences sont exactement les bugs classiques du dev mobile qui arrive sur le web. La table à mémoriser :

PropriétéDéfaut webDéfaut React Native
Mode de layoutflux normal — display: flex requisflexbox partout, d'office
flex-directionrowcolumn
align-itemsstretchstretch (pareil)
flex-shrink1 (les enfants se compriment)0 (ils ne se compriment pas)
box-sizingcontent-box (reset requis)comportement border-box d'office

Les deux lignes en gras et la ligne flex-shrink sont celles qui coûtent du temps : quand un layout se comporte « bizarrement » d'un côté ou de l'autre, c'est presque toujours l'une de ces cinq lignes.

🧭 Aparté : min-width: 0, l'incantation anti-débordement

Tu croiseras dans du CSS réel la déclaration mystérieuse min-width: 0 sur un enfant flex. Par défaut, un enfant flex a min-width: auto, c'est-à-dire « je refuse d'être plus étroit que mon contenu ». Un texte long d'un seul tenant (une URL, un nom de fichier) le rend alors incompressible : flex-shrink a beau vouloir le rétrécir, le contenu fait blocage et toute la rangée déborde. min-width: 0 lève ce plancher — le texte peut être coupé ou recevoir des points de suspension. En lecture : min-width: 0 sur un item flex = « ici, quelqu'un a combattu un débordement de texte ».

✍️ Exercice de lecture

La barre de réservation de RendezVous : une date, le nom du service (qui peut être long), et un bouton « Réserver ». Voici son CSS, puis les questions.

.barre {
  display: flex;
  align-items: center;
  gap: 12px;
}
.barre .date    { flex-shrink: 0; }  /* ex. « mardi 12 mars »          */
.barre .service { flex: 1; }         /* ex. « Consultation de suivi… » */
.barre .bouton  { flex-shrink: 0; }

Questions : (1) Dans quel sens les trois éléments sont-ils rangés, et pourquoi, alors qu'aucun flex-direction n'est écrit ? (2) Quel élément absorbe l'espace libre quand la barre est large ? (3) Le nom du service est très long et la barre déborde de l'écran malgré le flex: 1 — pourquoi, et quelle déclaration de l'aparté ci-dessus manque-t-il ?

Voir le corrigé

(1) Horizontalement, de gauche à droite : le défaut web de flex-direction est row. Un dev RN s'attendrait à une colonne — c'est exactement le piège des défauts opposés.

(2) .service, grâce à son flex: 1 (grow: 1) : la date et le bouton gardent leur taille naturelle (grow à 0 par défaut), et tout l'espace restant va au nom du service.

(3) À cause du plancher min-width: auto : l'enfant flex refuse d'être plus étroit que son contenu, donc un nom d'un seul tenant très long rend .service incompressible et pousse la rangée hors de l'écran. Il manque min-width: 0 sur .service (généralement accompagné d'un overflow: hidden; text-overflow: ellipsis; white-space: nowrap pour couper proprement avec des points de suspension).

🧠 Quiz éclair

On veut que le pied de page soit poussé en bas de la fenêtre même quand la page est courte. Le flex: 1 est pourtant bien là, et il ne pousse rien du tout. Pourquoi ?

.page {
  display: flex;
  flex-direction: column;
}
.page__contenu { flex: 1; }   /* censé absorber l'espace restant */
.page__pied    { padding: 16px; }

Parce que flex: 1 distribue l'espace disponible, et qu'ici il n'y en a aucun. .page n'a reçu aucune hauteur, et sur le web un bloc épouse par défaut la hauteur de son contenu : la taille du parent sur l'axe vertical est donc exactement la somme de ses enfants. Espace restant à répartir : zéro. Chacun garde sa taille naturelle, et le pied se colle sous le contenu au lieu de descendre en bas de l'écran.

Le correctif est sur le parent, jamais sur l'enfant : .page { min-height: 100vh; } — 100 % de la hauteur de la fenêtre — crée enfin de l'espace à distribuer. Le réflexe à installer, et il vaut des deux côtés : quand un flex: 1 ne fait rien, remonte d'un cran. C'est le même gotcha que celui du flex-1 qui s'effondre dans un parent sans hauteur contrainte, côté React Native.

Centrer, enfin

« Comment centre-t-on une div ? » est une vraie question d'entrevue, et la réponse tient en deux formules — pas une de plus.

Formule 1 — le centrage flex, pour centrer le contenu d'un conteneur, horizontalement et verticalement à la fois :

.parent {
  display: flex;
  justify-content: center; /* centre sur l'axe principal (horizontal en row) */
  align-items: center;     /* centre sur l'axe croisé (vertical en row)      */
}

Le centrage parfait en trois déclarations sur le parent. Seule condition : que le parent ait une hauteur — on ne peut pas centrer verticalement dans un conteneur qui épouse son contenu.

Formule 2 — margin: auto, pour centrer un bloc dans la page. Sur un élément de type bloc dont la largeur est bornée — par un width, ou par un max-width comme dans l'exemple ci-dessous —, margin-left: auto et margin-right: auto signifient « répartissez l'espace restant équitablement des deux côtés » — donc centrage horizontal. C'est le patron immémorial du contenu de page :

.page {
  max-width: 720px; /* jamais plus large que ça (confort de lecture) */
  margin: 0 auto;   /* 0 en vertical, auto = centrage en horizontal  */
}

Le duo max-width + margin: 0 auto : la colonne de contenu centrée qu'on retrouve sur pratiquement tous les sites, celui-ci compris.

Comment choisir ? Question de point de vue : le centrage flex se déclare sur le parent (« centre tout ce que je contiens ») ; le margin: auto se déclare sur l'enfant (« centre-moi dans l'espace qu'on me donne ») et ne fait que l'horizontal. En pratique : contenu de page → formule 2 ; tout le reste (icône dans un bouton, spinner, texte dans une pastille) → formule 1. Détail utile en entrevue : dans un conteneur flex, les marges auto absorbent l'espace libre dans toutes les directions, donc margin: auto sur un enfant flex unique le centre parfaitement.

📖 La formule

La rangée flexible

.row {
  display: flex;                  /* passe les enfants en flexbox        */
  align-items: center;            /* alignés verticalement au centre     */
  justify-content: space-between; /* premiers/derniers aux bords,        */
  gap: 12px;                      /* espace régulier entre les enfants   */
}

C'est le patron le plus tapé du frontend web — barres d'en-tête, lignes de liste, pieds de carte : un coup d'œil et tu le reconnais partout. En React Native il s'écrit exactement pareil, à un détail près : il faut ajouter flexDirection: 'row', puisque le défaut y est column.

📅 Dans RendezVous

RendezVous l'utilise mot pour mot pour chaque ligne de la page « Mes rendez-vous » : à gauche la date et le service, à droite le badge d'état et le bouton Annuler. display: flex met tout sur une ligne, align-items: center aligne texte, badge et bouton malgré leurs hauteurs différentes, justify-content: space-between pousse les informations à gauche et les actions à droite, gap évite que le badge colle au bouton. Ouvre les DevTools sur n'importe quel site de prise de rendez-vous : tu y retrouveras cette règle, au pixel près.

🧠 Quiz éclair

Le texte de la pastille est parfaitement centré verticalement… et reste collé à gauche. Quelle déclaration manque, et pourquoi celle qui est écrite ne fait-elle pas ce qu'on attendait d'elle ?

.pastille {
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 200px;
  height: 200px;
}

Il manque align-items: center;. Le piège est l'inversion des axes : justify-content agit toujours sur l'axe principal, et flex-direction: column a fait de la verticale l'axe principal — d'où le centrage vertical, effectivement obtenu. L'horizontale est devenue l'axe croisé, et c'est align-items qui la gouverne. En row, les deux rôles seraient exactement échangés.

C'est pour ça que le moyen mnémotechnique ne parle jamais d'horizontal ni de vertical : justify = axe principal, align = axe croisé ; c'est flex-direction qui dit lequel est lequel. Retiens aussi la condition tacite du centrage vertical, remplie ici par le height: 200px : sans hauteur, le conteneur épouse son contenu et il n'y a rien à centrer.

Lire le CSS de quelqu'un d'autre

Dernière compétence du module, et la plus rentable pour un test technique ou un premier jour de stage : ouvrir une feuille de style écrite par quelqu'un d'autre et s'y orienter vite. Un fichier réel fait des centaines de lignes ; on ne le lit pas de haut en bas. Stratégie en quatre temps.

  1. Repère le reset et les fondations, tout en haut : le box-sizing: border-box et les règles sur body (police, couleurs). Elles donnent l'ambiance et les valeurs héritées partout — car certaines propriétés (color, font-family, line-height) s'héritent : posées sur body, elles ruissellent sur tous les descendants qui ne les redéfinissent pas. D'où une seule règle qui typographie toute la page.
  2. Identifie les classes « racines » qui structurent la page — .layout, .sidebar, .content… généralement les premiers grands blocs après le reset, souvent des conteneurs flex. Elles donnent le squelette ; le reste est du remplissage.
  3. Suis une classe à la trace : Ctrl+F sur .carte et lis toutes ses occurrences — la règle de base, mais aussi .carte:hover ou un .services .carte qui la surcharge. Le style final d'un élément est la somme arbitrée de toutes les règles qui le visent, jamais une seule.
  4. Vérifie dans le navigateur, pas dans ta tête : clic droit → « Inspecter » → panneau Styles des DevTools. Il liste toutes les règles qui visent l'élément, de la plus forte à la plus faible, et barre les déclarations perdantes — tu vois l'arbitrage. L'onglet « Computed » dessine même le modèle de boîte avec ses valeurs réelles. Bonus : tu peux modifier les valeurs en direct (un rechargement efface tout) — quand tu te demandes « à quoi sert cette règle ? », désactive-la et regarde ce qui bouge. C'est le meilleur outil d'apprentissage du CSS qui existe.
✍️ Exercice de lecture

La carte de service de RendezVous, en CSS vanilla. Le viewport (l'écran) fait 360 px de large.

.carte {
  width: 320px;
  padding: 16px;
  border: 2px solid #e2e8f0;
  margin: 0 12px;
}

Questions : (1) Sans le reset border-box (donc en content-box, le défaut), quelle est la largeur visible de la carte (bordure comprise), et combien d'espace horizontal total occupe-t-elle avec ses marges ? Rentre-t-elle dans l'écran ? (2) Avec box-sizing: border-box, mêmes questions. (3) Dans les deux cas, la marge fait-elle partie de la « largeur » de la carte ?

Voir le corrigé

(1) En content-box, width: 320px ne mesure que le contenu. Largeur visible = 320 + 16 × 2 (padding) + 2 × 2 (bordure) = 356 px. Avec les marges : 356 + 12 × 2 = 380 px d'espace occupé — ça ne rentre PAS dans les 360 px de l'écran : débordement horizontal, la page « scrolle » de côté. Le grand classique.

(2) En border-box, width: 320px = le rectangle visible, padding et bordure compris : largeur visible = 320 px tout rond (le contenu se comprime à 320 − 32 − 4 = 284 px). Espace total avec marges : 320 + 24 = 344 px — ça rentre. Voilà, en un exemple chiffré, pourquoi tout le monde met border-box.

(3) Non, jamais : la margin est en dehors de la boîte dans les deux modes. box-sizing ne décide que du sort du padding et de la bordure ; la marge est toujours de l'espace entre les boîtes, en plus.

🎤 En entrevue

« Peux-tu m'expliquer le modèle de boîte ? »

« Chaque élément est un rectangle à quatre couches : le contenu au centre, puis le padding — la marge intérieure, celle qui porte le fond —, puis la bordure, et enfin la margin, qui est extérieure et transparente. »

« Mais le vrai point, c'est box-sizing. Par défaut, en content-box, width ne mesure que le contenu : le padding et la bordure viennent s'ajouter par-dessus. Donc un bloc à width: 300px avec 20 px de padding occupe 340 px, ce qui casse toutes les mises en page qu'on croyait calculées. C'est pour ça qu'on met box-sizing: border-box partout en première ligne du projet : width mesure alors le rectangle visible, bordure comprise, et le chiffre qu'on écrit est celui qu'on obtient. »

« Comment centres-tu un élément horizontalement et verticalement ? »

« Avec flexbox, sur le parent : display: flex, justify-content: center pour l'axe principal, align-items: center pour l'axe croisé. Avec une condition qu'on oublie souvent — le parent doit avoir une hauteur, sinon il fait exactement la taille de son contenu et il n'y a rien à centrer verticalement. »

« Pour le cas particulier "centrer un bloc de contenu dans la page", je ne sors même pas flexbox : max-width plus margin: 0 auto sur l'élément lui-même suffit. Et display: grid; place-items: center fait le travail en une déclaration — mais flexbox reste ce qu'on attend d'entendre. »

« Quelle est la différence entre margin et padding ? »

« Le padding est à l'intérieur de la boîte, entre le contenu et la bordure : il porte la couleur de fond et il fait partie de la surface cliquable — c'est pour ça qu'on agrandit un bouton avec du padding et jamais avec de la margin. La margin, elle, est à l'extérieur : de l'espace transparent entre la boîte et ses voisines. »

« En pratique je tranche avec une seule question : est-ce que je veux de l'air dans le composant, ou autour ? Dedans, c'est padding. Autour, c'est margin — ou mieux, un gap sur le parent flex, qui espace les enfants sans qu'aucun n'ait à porter de marge, et qui évite d'avoir à retirer la marge du dernier élément. »

À retenir

La cascade arbitre les règles en conflit : origine, puis spécificité (inline > id > classe > élément), puis ordre d'écriture. Chaque élément est une boîte — contenu, padding, bordure, margin — et box-sizing: border-box fait que width mesure le rectangle visible, comme React Native le fait d'office. Le web pose les éléments dans le flux normal tant qu'un conteneur ne déclare pas display: flex. Flexbox : le conteneur choisit l'axe (défaut web = row, RN = column), distribue (justify-content), aligne (align-items), espace (gap) ; les enfants négocient avec flex-grow/shrink/basis, dont le raccourci roi flex: 1. Centrer : flex sur le parent, ou max-width + margin: 0 auto pour un bloc de page. Devant le CSS d'autrui : reset, classes racines, Ctrl+F, puis DevTools > Styles pour voir la cascade arbitrer en vrai.

Et ailleurs : Ce module est du muscle pur pour les tests techniques : le modèle de boîte, la spécificité et « centrer une div » sont des questions posées telles quelles, et un « intègre cette maquette en CSS vanilla » se résout à 80 % avec la rangée flexible, gap et max-width + margin: 0 auto. Le transfert joue dans les deux sens : chaque classe Tailwind ou NativeWind est un alias d'une déclaration vue ici (items-center = align-items: center, flex-1 = flex: 1). Comprendre le CSS d'en dessous, c'est déboguer Tailwind sans tâtonner. Suite au module « CSS 2 — grid, responsive, variables ».

🗂️ L'aide-mémoire
flex-direction
vaut row sur le web, column en React Native
flex-shrink
vaut 1 sur le web (l'enfant cède), 0 en RN (il déborde)
justify-content
agit sur l'axe principal ; align-items sur l'axe croisé
box-sizing
défaut content-box : width exclut padding et bordure
Marges verticales
fusionnent entre blocs voisins — on garde la plus grande, pas la somme. Jamais dans un conteneur flex
Spécificité
id (100) > classe (10) > balise (1) ; à égalité, le dernier déclaré gagne
Centrer un bloc
max-width + margin: 0 auto (horizontal seulement)
Centrer un contenu
display: flex + justify-content + align-items — exige une hauteur pour le vertical