Comprendre comment positionner les éléments dans une page web. Du positionnement statique au positionnement sticky, en passant par les mystères du z-index et des contextes d'empilement.
position contrôle comment un élément est placé dans la page — statiquement (flux normal) ou selon un modèle de positionnement spécial (relatif, absolu, fixe ou sticky).
CSS offre 5 valeurs pour la propriété position. Chacune modifie la façon dont un élément interagit avec le flux normal (le flux par défaut où les blocs s'empilent de haut en bas et les inline de gauche à droite).
| Valeur | Hors du flux ? | Se déplace avec offset ? | Conteneur de référence |
|---|---|---|---|
static |
Non | Non | Flux normal |
relative |
Non | Oui (décalage visuel) | Lui-même (position d'origine) |
absolute |
Oui | Oui | Conteneur positionné le plus proche |
fixed |
Oui | Oui | Le viewport (fenêtre) |
sticky |
Non (tant que pas threshold) | Oui (au scroll) | Zone de scroll parente |
position: relative sert surtout à créer un contexte de positionnement pour les enfants absolute, et à créer un contexte de pile (stacking context) pour le z-index. Les offsets sont rarement utilisés pour le décalage visuel.
top, right, bottom, left et z-index sont ignorées.
.element {
position: static; /* Défaut — peut être omis */
}
FLUX NORMAL (position: static) ┌──────────────────────────────────────┐ │ ┌────────────────────────────────┐ │ │ │ Bloc 1 │ │ │ └────────────────────────────────┘ │ │ ┌────────────────────────────────┐ │ │ │ Bloc 2 │ │ │ └────────────────────────────────┘ │ │ ┌────────────────────────────────┐ │ │ │ Bloc 3 │ │ │ └────────────────────────────────┘ │ └──────────────────────────────────────┘ Les éléments s'empilent verticalement Selon l'ordre du HTML
Le flux normal — position: static est invisible
top, right, bottom, left le décalent visuellement par rapport à sa position d'origine. Crée un contexte de positionnement pour les enfants absolute.
.element {
position: relative;
top: 10px; /* Décalage vers le bas */
left: 20px; /* Décalage vers la droite */
}
position: relative + top: 10px + left: 20px
Position d'origine Position après décalage
(la place reste) (déplacement visuel)
┌──────────────────┐ ┌──────────────────┐
│ │ │ │
│ ┌──────────┐ │ → │ ┌──────────┐ │
│ │ Élément │ │ │ │ Élément │ │ ← décalé
│ └──────────┘ │ │ └──────────┘ │
│ │ │ ↑ 10px │
└──────────────────┘ └──────────────────┘
← 20px →
Le décalage relatif — l'espace d'origine est conservé
| Propriété | Direction | Effet |
|---|---|---|
top: 10px |
Vers le bas | Pousse l'élément 10px vers le bas depuis sa position |
right: 10px |
Vers la gauche | Pousse l'élément 10px vers la gauche depuis sa position |
bottom: 10px |
Vers le haut | Pousse l'élément 10px vers le haut depuis sa position |
left: 10px |
Vers la droite | Pousse l'élément 10px vers la droite depuis sa position |
position: relative, top: 10px pousse vers le bas (pas vers le haut). Avec position: absolute, c'est pareil — top: 0 colle à l'extérieur supérieur du conteneur parent.
position autre que static, ou le <html>).
.parent {
position: relative; /* Crée le contexte de positionnement */
}
.enfant {
position: absolute;
top: 0;
right: 0;
}
CONTENANT (position: relative) ┌──────────────────────────────────────┐ │ │ │ L'enfant absolu est placé │ │ par rapport aux bords de │ │ CE conteneur. │ │ │ │ ┌──────────┐ │ │ │ Enfant │ ← position: absolute │ │ absolute │ top: 0; right: 0 │ └──────────┘ │ │ │ └──────────────────────────────────────┘ Si aucun ancêtre n'est positionné, le conteneur est le <html>
L'élément absolu se positionne par rapport au conteneur positionné
.header-fixe {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
.retour-en-haut {
position: fixed;
bottom: 2rem;
right: 2rem;
}
┌─── VIEWPORT (fenêtre) ─────────────────┐ │ ┌─────────────────────────────────────┐ │ │ │ ═══ HEADER FIXE (top: 0) ═══════════│ │ ← reste ici │ └─────────────────────────────────────┘ │ au scroll │ │ │ Contenu scrollable : │ │ ┌───────────────────────────────────┐ │ │ │ Ligne 1 │ │ │ │ Ligne 2 │ │ │ │ Ligne 3 ← le contenu défile │ │ │ │ ... │ │ │ └───────────────────────────────────┘ │ │ │ │ ┌──────┐ │ │ │ ↑Top │ │ ← aussi fixe │ └──────┘ │ └─────────────────────────────────────────┘
position: fixed — ancré au viewport, ignore le scroll
transform, filter, will-change ou contain, l'élément fixed se positionne par rapport à cet ancêtre, pas le viewport ! C'est une cause fréquente de bugs.
relative tant qu'il est dans la zone visible, puis passe en fixed (collé au seuil défini) quand on dépasse son seuil de défilement (top, bottom, left, right).
.en-tete-colonne {
position: sticky;
top: 0; /* Se fixe en haut quand on scroll */
z-index: 10;
}
AVANT le scroll PENDANT le scroll ┌────────────────────┐ ┌────────────────────┐ │ ┌────────────────┐ │ │ ══ HEADER STICKY ══│ ← fixé au top: 0 │ │ HEADER STICKY │ │ ├────────────────────┤ │ ├────────────────┤ │ │ Item 5 │ │ │ Item 1 │ │ │ Item 6 │ │ │ Item 2 │ → │ Item 7 │ │ │ Item 3 │ SCROLL│ Item 8 │ │ │ Item 4 │ │ │ Item 9 │ │ └────────────────┘ │ │ Item 10 │ └────────────────────┘ └────────────────────┘
sticky — relatif tant que dans la zone, fixé quand on dépasse le seuil
z-index contrôle l'ordre d'empilement (stacking order) des éléments qui se chevauchent. Plus la valeur est élevée, plus l'élément est devant. Ne fonctionne que sur les éléments positionnés (pas static).
.element-derriere {
position: absolute;
z-index: 1;
}
.element-devant {
position: absolute;
z-index: 10;
}
z-index des enfants ne peuvent pas dépasser l'élément parent qui crée le contexte.
Ce qui crée un stacking context :
position avec une valeur autre que static + z-index ≠ autoopacity < 1transform ≠ nonefilter ≠ nonewill-change pour transform, opacity, etc.mix-blend-mode ≠ normalisolation: isolate (création explicite)┌─── CONTEXTE A (z-index: 1) ──────────────────────┐ │ │ │ enfant-A-1 (z-index: 999) │ │ enfant-A-2 (z-index: 5) │ │ │ │ ┌─── CONTEXTE B (z-index: 2) ───────────────┐ │ │ │ │ │ │ │ enfant-B-1 (z-index: 999) │ │ │ │ enfant-B-2 (z-index: 5) │ │ │ │ │ │ │ └────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────┘ Résultat visuel (de devant à derrière) : 1. enfant-A-1 (contexte A, z-index: 999) 2. enfant-B-1 (contexte B, z-index: 999 mais B < A) 3. enfant-A-2 (contexte A, z-index: 5) 4. enfant-B-2 (contexte B, z-index: 5) Le z-index: 999 de B ne peut PAS dépasser A car B est "bloqué" dans son contexte (z-index: 2 < 1 de A... non, 2 > 1) → Les enfants de B ne peuvent PAS dépasser B, et les enfants de A ne peuvent PAS dépasser A.
Les z-index sont comparés au sein du même contexte d'empilement
Ordre d'empilement du fond vers le premier plan
| Propriété | static | relative | absolute | fixed | sticky |
|---|---|---|---|---|---|
| Hors du flux | Non | Non | Oui | Oui | Non* |
| top/right/bottom/left | Ignoré | Décalage visuel | Positionnement | Positionnement | Seuil de scroll |
| z-index | Ignoré | Fonctionne | Fonctionne | Fonctionne | Fonctionne |
| Scroll avec la page | Oui | Oui | Oui | Non | Oui puis fixe |
| Espace réservé | Oui | Oui | Non | Non | Oui* |
| Conteneur de référence | — | Lui-même | Parent positionné | Viewport | Parent scrollable |
* sticky: hors du flux uniquement quand il est "collé"
Flux normal, pas de décalage
Décalé visuellement, place préservée
Hors du flux, positionné dans le parent
<div class="image-conteneur">
<img src="photo.jpg" alt="Photo">
<span class="badge">Nouveau</span>
</div>
.image-conteneur {
position: relative; /* Contexte pour l'enfant absolute */
display: inline-block;
}
.badge {
position: absolute;
top: 8px;
right: 8px;
background: red;
color: white;
padding: 4px 8px;
border-radius: 999px;
font-size: 0.75rem;
}
<button class="tooltip-trigger">
Survolez-moi
<span class="tooltip-text">Ceci est un tooltip !</span>
</button>
.tooltip-trigger {
position: relative;
}
.tooltip-text {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: #1e293b;
color: white;
padding: 6px 12px;
border-radius: 4px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.tooltip-text::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #1e293b;
}
.tooltip-trigger:hover .tooltip-text {
opacity: 1;
}
.modal-backdrop {
position: fixed;
inset: 0; /* top: 0; right: 0; bottom: 0; left: 0 */
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-box {
background: white;
padding: 2rem;
border-radius: 8px;
max-width: 500px;
width: 90%;
}
.layout {
display: flex;
gap: 2rem;
}
.sidebar {
position: sticky;
top: 1rem;
align-self: flex-start;
width: 250px;
}
.contenu {
flex: 1;
}
.header {
position: sticky;
top: 0;
z-index: 100;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Les dropdowns du menu ont besoin d'un z-index plus élevé */
.nav-dropdown {
position: absolute;
top: 100%;
z-index: 200;
}
/* ❌ FAUX — z-index est ignoré */
.bloc {
z-index: 100;
}
/* ✅ CORRECT */
.bloc {
position: relative; /* ou absolute, fixed, sticky */
z-index: 100;
}
/* ❌ MAUVAISE PRATIQUE */
.dropdown { z-index: 999999; }
.modal { z-index: 9999999; }
.header { z-index: 99999; }
/* ✅ BON — Système de z-index hiérarchique */
:root {
--z-header: 100;
--z-dropdown: 200;
--z-modal: 300;
--z-notification: 400;
}
.header { z-index: var(--z-header); }
.dropdown { z-index: var(--z-dropdown); }
.modal { z-index: var(--z-modal); }
.notification { z-index: var(--z-notification); }
/* ❌ Le header fixe ne marche plus ! */
.parent-avec-transform {
transform: scale(1);
}
.header-fixe {
position: fixed;
top: 0;
/* Se positionne par rapport au parent, pas au viewport ! */
}
/* ✅ Solution : éviter transform sur les ancêtres
ou utiliser un autre positionnement */
/* ❌ L'enfant se positionne par rapport au <html> */
.conteneur {
/* pas de position */
}
.conteneur .enfant {
position: absolute;
top: 0; left: 0;
/* → en haut à gauche de TOUTE la page ! */
}
/* ✅ Ajouter position: relative au parent */
.conteneur {
position: relative;
}
/* ❌ Incomplet — peut laisser des trous */
.overlay {
position: fixed;
top: 0;
left: 0;
/* bottom et right non définis ! */
}
/* ✅ Utiliser inset pour les 4 côtés */
.overlay {
position: fixed;
inset: 0;
}
/* ❌ Le parent empêche sticky de marcher */
.parent {
overflow: hidden; /* ou overflow: auto, scroll */
}
.enfant {
position: sticky;
top: 0;
/* Ne fonctionnera jamais ! */
}
/* ✅ Le parent ne doit pas avoir overflow caché
(sauf si c'est le conteneur de scroll voulu) */
/* Établir des niveaux clairs au début du projet */
:root {
--z-base: 1;
--z-dropdown: 100;
--z-sticky: 200;
--z-header: 300;
--z-overlay: 400;
--z-modal: 500;
--z-toast: 600;
--z-tooltip: 700;
}
/* Utiliser les variables partout */
.header { z-index: var(--z-header); }
.dropdown { z-index: var(--z-dropdown); }
.modal-backdrop { z-index: var(--z-overlay); }
.modal-content { z-index: var(--z-modal); }
.toast { z-index: var(--z-toast); }
/* Utiliser isolation pour isoler un composant */
.composant {
isolation: isolate;
/* Crée un stacking context sans effet visuel */
/* Les z-index internes ne déborderont pas */
}
relative, badge absoluterelative, tooltip absolutesticky + z-indexfixed + inset: 0sticky + align-self: flex-startfixed + bottom/rightrelative (trigger) + absolute (menu)fixed + z-index élevé/* ❌ Lent (déclenche le layout) */
.enfant {
transition: top 0.3s, left 0.3s;
}
/* ✅ Rapide (GPU accelerated) */
.enfant {
transition: transform 0.3s;
}
.enfant:hover {
transform: translate(20px, 10px);
}
Défi 1 : Tooltip pure CSS
Créez un tooltip qui apparaît au survol d'un bouton, positionné au-dessus avec une flèche vers le bas.
<button class="tooltip-trigger">
Info
<span class="tooltip-text">Tooltip ici</span>
</button>
.tooltip-trigger { position: relative; }
.tooltip-text {
position: absolute;
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.tooltip-trigger:hover .tooltip-text { opacity: 1; }
Défi 2 : Badge sur image
Créez une carte avec une image et un badge "Promo" positionné en haut à droite.
Défi 3 : Header sticky
Créez un header qui reste collé en haut au scroll avec un z-index suffisant pour rester au-dessus du contenu.
Défi 4 : Overlay modal
Créez un overlay semi-transparent qui couvre toute la page avec une boîte de dialogue centrée.
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: white;
padding: 2rem;
border-radius: 8px;
min-width: 300px;
}
Défi 5 : Menu dropdown
Créez un menu déroulant qui s'affiche au clic sur un bouton, positionné juste en dessous.
Projet 1 : Sticky header + sidebar
Créez une page avec un header sticky, une sidebar sticky, et un contenu long qui défile. La sidebar doit rester visible quand on scroll.
Projet 2 : Modal avec overlay
Créez un bouton qui ouvre une modale centrée avec un fond semi-transparent. La modale doit avoir un titre, un contenu et un bouton de fermeture.
Projet 3 : Galerie avec overlays
Créez une galerie d'images où chaque image a un badge (position: absolute) et un overlay au survol montrant le titre et la description.
Projet 4 : Tooltip system
Créez un système de tooltips avec 4 positions (top, bottom, left, right), chacune avec une flèche. Utilisez les propriétés de positionnement et le pseudo-élément ::after pour la flèche.
Projet 5 : Notification popups
Créez des notifications positionnées en fixed en haut à droite, avec un système de z-index pour qu'elles apparaissent au-dessus du contenu et les unes au-dessus des autres.
| Concept | Propriété | Usage principal |
|---|---|---|
| Positionnement | position |
static, relative, absolute, fixed, sticky |
| Offset | top, right, bottom, left |
Décalage ou positionnement |
| Empilement | z-index |
Ordre avant/arrière-plan |
| Couche complète | inset |
Raccourci pour top+right+bottom+left |
| Contexte d'empilement | isolation |
Créer un contexte isolé |
position ?absolute ?z-index ne fonctionne pas sur un élément static ?position: fixed peut ne pas fonctionner ?relative et absolute ?staticabsolute, fixed et sticky (quand collé)position ≠ static), ou le <html>z-index ne fonctionne que sur les éléments positionnésposition: sticky + top: 0 + z-indextransform, filter ou will-change, l'élément se positionne par rapport à cet ancêtreinsetisolation: isolaterelative reste dans le flux (place préservée), absolute en sort/* Badge sur image */
.parent { position: relative; }
.badge { position: absolute; top: 8px; right: 8px; }
/* Tooltip */
.trigger { position: relative; }
.tooltip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
}
/* Overlay modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
}
/* Header sticky */
.header {
position: sticky;
top: 0;
z-index: 100;
}
/* Sidebar sticky */
.sidebar {
position: sticky;
top: 1rem;
align-self: flex-start;
}
/* Retour en haut */
.back-top {
position: fixed;
bottom: 2rem;
right: 2rem;
}
/* Système de z-index */
:root {
--z-dropdown: 100;
--z-header: 200;
--z-modal: 300;
--z-toast: 400;
}