Skip to Content
FrontendMotifs de mise en page

Motifs de mise en page CSS

Fiches pratiques pour les structures de layout les plus courantes en CSS pur (flexbox et grid). Chaque motif inclut le code CSS minimal et les pièges associés.

Ces exemples sont indépendants de tout framework. Ils reposent uniquement sur les propriétés standard de flexbox et de grid.


Flexbox — Alignement vertical au centre

Le cas d’école, mais on l’oublie souvent :

.center-box { display: flex; justify-content: center; align-items: center; min-height: 100dvh; }

Piège : height: 100vh sur mobile coupe le contenu au-dessus de la barre d’adresse. Préférer min-height: 100dvh (viewport dynamique, inclut les barres).


.layout-sidebar { display: flex; min-height: 100dvh; } .sidebar { width: 260px; flex-shrink: 0; background: #f4f4f5; padding: 1rem; } .main-content { flex: 1; padding: 1.5rem; overflow-y: auto; }

Piège : si la barre latérale a flex-shrink: 1 (par défaut), un contenu long du principal peut écraser la sidebar. Toujours fixer flex-shrink: 0.


La navigation latérale reste ancrée en haut de l’écran pendant le défilement du contenu :

.sidebar { position: sticky; top: 1rem; height: calc(100dvh - 2rem); overflow-y: auto; padding: 1rem; } .layout-sticky { display: flex; min-height: 100dvh; } .main-content { flex: 1; padding: 1.5rem; max-width: 800px; }

Piège : sans top explicite, sticky se comporte comme static (pas d’effet). Sans height limitée, la sidebar pousse le contenu au-delà du viewport. Utiliser calc() avec dvh pour s’adapter aux barres mobiles.


Le contenu principal est déplacé latéralement par la sidebar quand celle-ci apparaît. Aucun changement de positionnement absolu : tout reste en flux flex.

.layout-push { display: flex; min-height: 100dvh; } .sidebar-push { width: 0; overflow: hidden; flex-shrink: 0; background: #1e293b; color: #f8fafc; transition: width 0.25s ease; padding: 0 1rem; } .sidebar-push.is-open { width: 260px; padding: 1rem; } .main-push { flex: 1; transition: margin-left 0.25s ease; padding: 1.5rem; } .main-push.is-pushed { margin-left: 260px; }
<!-- HTML minimal : un toggle sur un bouton --> <button id="toggle" onclick="document.getElementById('sidebar').classList.toggle('is-open'); document.getElementById('main').classList.toggle('is-pushed')">☰ Menu</button> <div class="layout-push"> <aside id="sidebar" class="sidebar-push">…</aside> <div id="main" class="main-push">…</div> </div>

Piège : animer width et margin simultanément sans transition. Sans transition, le glissement est instantané et non perceptible. Si le contenu a une largeur fixe (width: 260px dans .is-open), la transition sur width fonctionne sans transition: width 0.25s — mais c’est une erreur de s’y fier sur tous les navigateurs anciens.


La sidebar glisse par-dessus le contenu principal sans le déplacer. Idéale quand le contenu n’a pas besoin de se recaler (panneaux de filtres, menus contextuels).

.layout-overlay { position: relative; min-height: 100dvh; } .sidebar-overlay { position: absolute; top: 0; left: 0; width: 280px; height: 100%; background: #0f172a; color: #f8fafc; transform: translateX(-100%); transition: transform 0.3s ease; z-index: 30; padding: 1rem; overflow-y: auto; } .sidebar-overlay.is-open { transform: translateX(0); } /* Calque semi-transparent derrière la sidebar */ .overlay-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4); opacity: 0; pointer-events: none; transition: opacity 0.3s ease; z-index: 20; } .overlay-backdrop.is-visible { opacity: 1; pointer-events: auto; } .main-overlay { padding: 1.5rem; min-height: 100dvh; }

Piège : oublier z-index hiérarchique. Le calque (backdrop) doit avoir un z-index inférieur à la sidebar mais supérieur au contenu principal. Si z-index n’est pas appliqué sur le parent, il faut aussi fixer position (absolute/fixed/relative) sur les éléments concernés, sinon z-index est sans effet.


La sidebar réside entièrement hors de l’écran et n’apparaît que lorsqu’on la fait glisser. La variante mobile-first, sans JavaScript si possible.

/* État caché par défaut */ .offcanvas { position: fixed; top: 0; left: 0; width: 280px; height: 100dvh; background: #1e293b; color: #f8fafc; transform: translateX(-100%); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 40; padding: 1rem; overflow-y: auto; } /* Activation par checkbox (pure CSS, pas de JS) */ .offcanvas-toggle { position: absolute; opacity: 0; width: 0; height: 0; } .offcanvas-toggle:checked ~ .offcanvas { transform: translateX(0); } .offcanvas-toggle:checked ~ .offcanvas-backdrop { opacity: 1; pointer-events: auto; } .offcanvas-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4); opacity: 0; pointer-events: none; transition: opacity 0.3s ease; z-index: 30; } /* Bouton hamburger (label de la checkbox) */ .hamburger { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; cursor: pointer; border: none; background: transparent; } /* Accessibilité : focus-visible sur le toggle */ .offcanvas-toggle:focus-visible + .offcanvas-backdrop { outline: 2px solid #60a5fa; outline-offset: 2px; }
<input type="checkbox" id="offcanvas-toggle" class="offcanvas-toggle" aria-label="Ouvrir le menu"> <label for="offcanvas-toggle" class="hamburger" aria-label="Menu"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/> </svg> </label> <div class="offcanvas-backdrop"></div> <nav class="offcanvas" aria-label="Navigation principale">…</nav>

Piège : la checkbox pure-CSS ne fonctionne que si l’élément input et le .offcanvas sont frères directs (sélecteur ~). Si un conteneur intermédiaire sépare les deux, le sélecteur échoue. Solution : placer le &lt;input&gt; juste avant &lt;nav class="offcanvas"&gt;, dans le même parent.


Version mobile-first (uniquement CSS) : la navigation est cachée en dessous du logo et apparaît au clic sur le hamburger. Sur desktop (>768px), les liens sont visibles en ligne.

.navbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; background: #0f172a; color: #f8fafc; } .navbar .brand { font-weight: 700; font-size: 1.125rem; color: inherit; text-decoration: none; flex-shrink: 0; } /* Toggle (caché) */ .navbar-toggle { position: absolute; opacity: 0; width: 0; height: 0; } /* Bouton visible uniquement en mobile */ .navbar-burger { display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; cursor: pointer; border: none; background: transparent; color: inherit; } /* Liens cachés par défaut (mobile) */ .navbar .links { display: flex; flex-direction: column; width: 100%; max-height: 0; overflow: hidden; transition: max-height 0.3s ease; gap: 0.25rem; } .navbar .links a { color: inherit; text-decoration: none; padding: 0.5rem 0.75rem; border-radius: 4px; } .navbar .links a:hover { background: rgba(255, 255, 255, 0.1); } /* Activation via checkbox */ .navbar-toggle:checked ~ .navbar .links { max-height: 500px; } /* Desktop : liens visibles en ligne, burger caché */ @media (min-width: 768px) { .navbar .links { flex-direction: row; width: auto; max-height: none; overflow: visible; gap: 0.25rem; } .navbar-burger { display: none; } }
<nav class="navbar" aria-label="Navigation"> <a href="/" class="brand">MonApp</a> <input type="checkbox" id="nav-toggle" class="navbar-toggle" aria-label="Menu"> <label for="nav-toggle" class="navbar-burger" aria-label="Ouvrir la navigation"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/> </svg> </label> <div class="links"> <a href="/dashboard">Tableau de bord</a> <a href="/projets">Projets</a> <a href="/parametres">Paramètres</a> </div> </nav>

Piège : utiliser display: none pour cacher la liste des liens. Les transitions ne fonctionnent pas sur display, donc l’animation d’ouverture/fermeture est impossible. Remplacer par max-height + overflow: hidden qui permet une transition fluide.

Piège : oublier que le sélecteur ~ exige que la checkbox et le &lt;div class="links"&gt; soient frères dans le DOM. Si un &lt;label&gt; ou tout autre nœud les sépare, le CSS ne s’applique pas. Placer le &lt;input&gt; comme premier enfant du &lt;nav&gt; pour garantir la parenté.


Le footer reste collé en bas même quand le contenu est court :

.wrapper { display: flex; flex-direction: column; min-height: 100dvh; } header, footer { flex-shrink: 0; } main { flex: 1; padding: 2rem; }

Aucun HTML spécial requis — cette structure reste mince dans le DOM.


.navbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem 1rem; } .navbar .brand { flex-shrink: 0; } .navbar .links { display: flex; gap: 1rem; flex-wrap: wrap; } /* Pas besoin de media query pour cet exemple simple : flex-wrap: wrap + gap gèrent le rétrécissement */

Piège : oublier flex-wrap: wrap sur le conteneur. Les liens sortent de l’écran et créent un débordement horizontal.


Colonnes de hauteur égale

Avec flexbox, les enfants s’étirent à la hauteur du parent par défaut via align-items: stretch. Il n’y a rien à coder :

.equal-height { display: flex; gap: 1rem; }

Piège : utiliser height: 100% sur les enfants. Cela échoue si le parent n’a pas de hauteur explicite. Laisser le stretch flexbox faire le travail.


Grid — Galerie de cartes

.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; padding: 1.5rem; } .card { background: var(--card-bg, #fff); border-radius: 8px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); }
.card img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 4px 4px 0 0; }

Piège : utiliser width: 100% sans aspect-ratio. La carte varie en hauteur au chargement de chaque image, provoquant un « layout shift » (CLS) visible à l’arrivée de la page. object-fit: cover recadre l’image dans le ratio sans la déformer.

Aucune media query nécessaire — auto-fit + minmax réorganisent les cartes à chaque largeur.

Piège : confondre auto-fit et auto-fill. auto-fill réserve des colonnes vides, auto-fit fait coller les cartes ensemble. Utiliser auto-fit dans 95 % des cas.


Grid — Dashboard layout

Structure classique admin :

.dashboard { display: grid; grid-template-columns: 240px 1fr; grid-template-rows: auto 1fr; grid-template-areas: "sidebar main" "sidebar main"; min-height: 100dvh; } .sidebar-nav { grid-area: sidebar; } .header { grid-area: header; grid-column: 1 / -1; } .main-content { grid-area: main; overflow-y: auto; padding: 1.5rem; }

Piège : oublier grid-template-areas sans définir grid-area sur les enfants. Le grid se crée, mais rien ne s’y place.


Grille masonry (pseudo-masonry avec CSS grid)

Vrai masonry = distribution automatique vertical (non supporté nativement). Fallback grid propre :

.masonry-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; align-items: start; } .masonry-grid .item { /* Chaque item garde sa hauteur naturelle, pas de stretch */ }

Piège : utiliser align-items: stretch (valeur par défaut). Les items courts s’étirent à la hauteur du plus grand frère, cassant l’effet masonry.


Hero + sections (layout en grille à 2 colonnes)

.hero-section { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: center; padding: 3rem 2rem; } .hero-text h1 { font-size: 2.5rem; } .hero-media img { width: 100%; border-radius: 8px; } /* Responsive : une seule colonne en mobile */ @media (max-width: 640px) { .hero-section { grid-template-columns: 1fr; } }

Tailles fluides avec clamp()

Éviter les media queries pour la typographie et les espacements courants :

:root { --fs-title: clamp(1.5rem, 4vw, 2.5rem); --fs-body: clamp(0.9rem, 2vw, 1.125rem); --spacing: clamp(1rem, 3vw, 2rem); } h1 { font-size: var(--fs-title); } p { font-size: var(--fs-body); } .container { padding: var(--spacing); }

Piège : utiliser vw seul (sans clamp). Le texte devient illisible en très grand écran (>1500px) et trop petit en mobile (<320px).


Pièges récurrents à mémoire courte

PiègeSymptômeCorrection
height: 100vh sur mobileContenu coupé par la barre d’adressemin-height: 100dvh
box-sizing non globalCartes plus larges que leur conteneur*, *::before, *::after { box-sizing: border-box }
flex-wrap absentÉléments sortent de l’écran en étroitAjouter flex-wrap: wrap
100% sur un enfant flexChild s’étire sans limiteUtiliser flex-basis ou min-width/max-width
z-index sans positionEffet invisibleAjouter position: relative (ou absolute/fixed)
gap sur un conteneur non-flex/non-gridGap invisibleVérifier que le parent a display: flex ou display: grid
Débordement horizontalScroll horizontal inexpliquébody { overflow-x: hidden } en dépannage

Validation : chaque motif peut être copié-collé dans un fichier HTML + CSS minimal pour test immédiat.