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: 100vhsur mobile coupe le contenu au-dessus de la barre d’adresse. Préférermin-height: 100dvh(viewport dynamique, inclut les barres).
Sidebar + contenu principal (side by side)
.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 fixerflex-shrink: 0.
Sidebar sticky (navigation qui reste visible)
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
topexplicite,stickyse comporte commestatic(pas d’effet). Sansheightlimitée, la sidebar pousse le contenu au-delà du viewport. Utilisercalc()avecdvhpour s’adapter aux barres mobiles.
Sidebar push (poussoir)
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
widthetmarginsimultanément sanstransition. Sans transition, le glissement est instantané et non perceptible. Si le contenu a une largeur fixe (width: 260pxdans.is-open), la transition surwidthfonctionne sanstransition: width 0.25s— mais c’est une erreur de s’y fier sur tous les navigateurs anciens.
Sidebar overlay (superposition)
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-indexhiérarchique. Le calque (backdrop) doit avoir un z-index inférieur à la sidebar mais supérieur au contenu principal. Siz-indexn’est pas appliqué sur le parent, il faut aussi fixerposition(absolute/fixed/relative) sur les éléments concernés, sinonz-indexest sans effet.
Sidebar off-canvas (glissement depuis le bord)
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
inputet le.offcanvassont frères directs (sélecteur~). Si un conteneur intermédiaire sépare les deux, le sélecteur échoue. Solution : placer le<input>juste avant<nav class="offcanvas">, dans le même parent.
Navbar hamburger — variante mobile-first
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: nonepour cacher la liste des liens. Les transitions ne fonctionnent pas surdisplay, donc l’animation d’ouverture/fermeture est impossible. Remplacer parmax-height+overflow: hiddenqui permet une transition fluide.
Piège : oublier que le sélecteur
~exige que la checkbox et le<div class="links">soient frères dans le DOM. Si un<label>ou tout autre nœud les sépare, le CSS ne s’applique pas. Placer le<input>comme premier enfant du<nav>pour garantir la parenté.
Header + nav + footer (wrapper élastique)
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 responsive (colonnes → empilé)
.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: wrapsur 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%sansaspect-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: coverrecadre 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-fitetauto-fill.auto-fillréserve des colonnes vides,auto-fitfait coller les cartes ensemble. Utiliserauto-fitdans 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-areassans définirgrid-areasur 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
vwseul (sansclamp). Le texte devient illisible en très grand écran (>1500px) et trop petit en mobile (<320px).
Pièges récurrents à mémoire courte
| Piège | Symptôme | Correction |
|---|---|---|
height: 100vh sur mobile | Contenu coupé par la barre d’adresse | min-height: 100dvh |
box-sizing non global | Cartes plus larges que leur conteneur | *, *::before, *::after { box-sizing: border-box } |
flex-wrap absent | Éléments sortent de l’écran en étroit | Ajouter flex-wrap: wrap |
100% sur un enfant flex | Child s’étire sans limite | Utiliser flex-basis ou min-width/max-width |
z-index sans position | Effet invisible | Ajouter position: relative (ou absolute/fixed) |
gap sur un conteneur non-flex/non-grid | Gap invisible | Vérifier que le parent a display: flex ou display: grid |
| Débordement horizontal | Scroll 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.