Skip to main content

Chapitre 58 : Maîtrise du Stacking Context et z-index

Gestion avancée de l'ordre d'empilement des éléments superposés et résolution des conflits de profondeur.

Le z-index est l'une des propriétés les plus mal comprises du CSS. Pour un développeur senior, comprendre le z-index ne signifie pas savoir mettre z-index: 9999 pour "forcer" l'affichage d'un élément, mais comprendre la mécanique du Stacking Context (contexte d'empilement). Sans cette notion, vous vous retrouverez face à des bugs inexplicables où un élément refuse de passer au-dessus d'un autre, malgré une valeur de z-index astronomique.

Le Stacking Context

1. Quoi

Un Stacking Context est une hiérarchie tridimensionnelle virtuelle créée par le navigateur pour déterminer l'ordre dans lequel les éléments sont dessinés le long de l'axe Z (la profondeur).

Contrairement à une idée reçue, le z-index n'est pas global à la page. Il est relatif au contexte d'empilement le plus proche dans lequel l'élément est contenu. On peut comparer cela à des "couches" de calques : si un calque parent est placé derrière un autre calque, aucun enfant de ce premier calque ne pourra jamais passer devant le second, peu importe son propre z-index.

2. Pourquoi

Dans des interfaces complexes (Dashboards, SaaS, E-commerce), on manipule constamment des éléments superposés :

  • Modales et Overlays.
  • Tooltips et Menus déroulants.
  • Sticky headers et Floating Action Buttons (FAB).
  • Animations de cartes avec effets de survol.

Une mauvaise gestion des contextes d'empilement mène à la "guerre des z-index", où les développeurs augmentent arbitrairement les valeurs (z-index: 1000, puis 2000, puis 999999), rendant le code impossible à maintenir et créant des régressions visuelles dès qu'un nouveau composant est ajouté.

3. Comment

A. Ce qui crée un Stacking Context

Un contexte d'empilement n'est pas créé uniquement par z-index. Voici les déclencheurs principaux :

  1. L'élément racine (<html>).
  2. Positionnement + z-index : Un élément positionné (relative, absolute, fixed, sticky) avec un z-index différent de auto.
  3. Positionnement Fixed : Un élément position: fixed (dans la plupart des navigateurs modernes).
  4. Opacité : Un élément avec une valeur d' opacity inférieure à 1.
  5. Transformations : Un élément avec transform différent de none (ex: scale, translate, rotate).
  6. Filtres : Un élément avec filter différent de none.
  7. Flex/Grid Children : Un enfant d'un conteneur flex ou grid avec un z-index différent de auto (même sans position: relative).
  8. Will-change : Un élément avec will-change dont la valeur suggère la création d'un contexte (ex: will-change: transform).

B. Cas concret : Architecture de couches (Layering)

Pour éviter le chaos, la meilleure pratique senior consiste à définir des variables CSS pour les couches sémantiques.

:root {
/* On définit des paliers pour éviter les collisions */
--z-index-below: -1;
--z-index-base: 1;
--z-index-dropdown: 100;
--z-index-sticky: 200;
--z-index-modal-backdrop: 1000;
--z-index-modal: 1100;
--z-index-tooltip: 2000;
}

.card {
position: relative;
z-index: var(--z-index-base);
}

.card__tooltip {
position: absolute;
/* Le tooltip est relatif à .card, mais on utilise une variable pour la cohérence */
z-index: var(--z-index-tooltip);
top: -20px;
}

.global-modal {
position: fixed;
z-index: var(--z-index-modal);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

C. Limitations et Pièges

Le piège majeur est l'isolation. Si vous appliquez opacity: 0.9 ou transform: translateZ(0) sur un conteneur, vous créez un nouveau Stacking Context.

Si ce conteneur a un z-index: 1 et qu'un élément frère a un z-index: 2, tous les enfants du premier conteneur resteront en dessous du frère, même si l'enfant a un z-index: 999999. L'enfant est "prisonnier" du contexte de son parent.

4. Zone de Danger

L'erreur commune : Le "Z-index Arms Race" Utiliser z-index: 9999 pour résoudre un problème d'affichage. Cela indique généralement que vous ne comprenez pas où se situe le Stacking Context et que vous tentez de "forcer" le rendu. Cela cassera dès qu'un autre développeur fera la même chose pour un autre élément.

La bonne pratique : L'aplatissement ou le Portaling Si un élément (comme une modale) doit être au-dessus de tout, ne tentez pas de lutter contre les contextes d'empilement imbriqués. Utilisez un Portal (concept React/Vue/Angular) pour déplacer l'élément HTML tout en bas du <body>. Ainsi, il appartient au contexte racine et son z-index sera comparé aux autres éléments de premier niveau.

Hiérarchie des Stacking Contexts


Questions clés

1. Pourquoi un élément avec z-index: 999 peut-il s'afficher derrière un élément avec z-index: 1 ?

Découvrir la réponse

Cela arrive lorsque l'élément avec z-index: 999 est contenu dans un Stacking Context dont le z-index est inférieur à celui de l'élément avec z-index: 1. L'ordre d'empilement est résolu d'abord au niveau des parents avant d'être résolu entre les enfants.

2. Quelle est la différence entre z-index: auto et z-index: 0 ?

Découvrir la réponse

z-index: auto (valeur par défaut) signifie que l'élément ne crée pas de nouveau Stacking Context (sauf s'il est positionné fixed ou sticky). L'élément est alors traité comme s'il n'avait pas de z-index et suit l'ordre naturel du DOM. z-index: 0 crée un Stacking Context, isolant ainsi ses enfants.

3. Est-ce que transform: scale(1) crée un Stacking Context ?

Découvrir la réponse

Oui. Toute valeur de transform différente de none crée un nouveau contexte d'empilement, même si la transformation ne modifie visuellement rien (comme un scale de 1). C'est un piège classique lors de l'optimisation des performances avec will-change.

4. Comment forcer un élément à être "au-dessus de tout" sans utiliser de valeurs de z-index géantes ?

Découvrir la réponse

La solution architecturale est le "Portaling" : déplacer l'élément dans le DOM pour qu'il soit un enfant direct de <body>. Ainsi, il concourt dans le contexte d'empilement racine, où un z-index modeste (ex: 100) suffira généralement.


Mise en pratique

Exercice 1 : Reproduction guidée (Niveau Facile)

Créez trois boîtes superposées. La boîte A doit être en dessous, la boîte B au milieu, et la boîte C au-dessus. Utilisez des variables CSS pour gérer les z-index.

HTML de départ :

<div class="scene">
<div class="box box-a">A</div>
<div class="box box-b">B</div>
<div class="box box-c">C</div>
</div>
/* CSS de départ — à compléter */
.scene {
position: relative;
height: 180px;
}

.box {
position: absolute;
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: bold;
color: white;
}

/* TODO : Définir les variables --z-layer-bottom, --z-layer-mid, --z-layer-top dans :root */
/* TODO : Appliquer background, position (top/left) et z-index à chaque boîte */
Découvrir la solution commentée
:root {
--z-layer-bottom: 1;
--z-layer-mid: 2;
--z-layer-top: 3;
}

.box {
position: absolute;
width: 100px;
height: 100px;
}

.box-a {
background: red;
z-index: var(--z-layer-bottom);
top: 0; left: 0;
}

.box-b {
background: blue;
z-index: var(--z-layer-mid);
top: 20px; left: 20px;
}

.box-c {
background: green;
z-index: var(--z-layer-top);
top: 40px; left: 40px;
}

Exercice 2 : Adaptation et Debug (Niveau Intermédiaire)

Vous avez un composant Card qui a un transform: scale(1.05) au hover. À l'intérieur de cette carte, vous avez un Tooltip avec z-index: 1000. Cependant, lorsque la carte est survolée, le tooltip passe derrière un header adjacent qui a un z-index: 10. Expliquez pourquoi et corrigez le problème sans modifier le z-index du header.

Découvrir la solution commentée

Explication : Le transform: scale(1.05) crée un nouveau Stacking Context sur la .card. Le Tooltip, bien qu'ayant un z-index: 1000, est maintenant relatif à la carte. Si la carte elle-même n'a pas de z-index défini (ou un z-index inférieur à 10), elle et tous ses enfants passeront derrière le header.

Solution : Il faut donner un z-index à la carte lors du hover pour qu'elle passe devant le header.

.card {
position: relative;
transition: transform 0.2s;
}

.card:hover {
transform: scale(1.05);
/* On élève le contexte d'empilement de la carte entière */
/* pour que ses enfants (le tooltip) puissent être visibles */
z-index: 11;
}

.tooltip {
position: absolute;
z-index: 1000; /* Reste utile pour l'ordre interne à la carte */
}

Exercice 3 : Conception d'un système de couches (Niveau Senior)

Concevez une architecture CSS pour une application complexe comprenant :

  • Un fond de page.
  • Des éléments de contenu.
  • Un menu de navigation sticky.
  • Des modales avec un overlay sombre.
  • Des notifications "Toast" qui doivent être au-dessus des modales.

Utilisez une approche basée sur des variables et assurez-vous qu'aucune valeur ne dépasse 1000.

Ce qui doit être présent dans votre solution :

ÉlémentClasse CSSpositionVariable z-indexValeur suggérée
Décoration de fond.background-decorationabsolute--z-index-negative-1
Contenu principal.main-contentrelative--z-index-content1
Navigation sticky.main-navsticky + top: 0--z-index-navigation100
Overlay sombre.modal-backdropfixed + inset: 0--z-index-overlay500
Fenêtre modale.modal-contentfixed + centré--z-index-modal510
Conteneur Toast.toast-containerfixed + bottom/right--z-index-toast600
Rappel : pourquoi séparer overlay (500) et modal (510) ?

L'overlay et la modale doivent tous deux être dans le même Stacking Context (racine), mais la modale doit passer AU-DESSUS de l'overlay qui l'assombrit. Un écart de 10 points suffit et laisse de la place pour de futures couches intermédiaires.

HTML de départ :

<body>

<!-- Décoration de fond (ex: motif, gradient) -->
<div class="background-decoration"></div>

<!-- Navigation sticky -->
<nav class="main-nav">
<a href="#">Accueil</a>
<a href="#">Produits</a>
<a href="#">Contact</a>
</nav>

<!-- Contenu principal -->
<main class="main-content">
<h1>Tableau de bord</h1>
<p>Contenu de la page...</p>
<button onclick="document.querySelector('.modal-backdrop').style.display='block'; document.querySelector('.modal-content').style.display='block'">
Ouvrir une modale
</button>
</main>

<!-- Overlay de la modale (fond sombre) -->
<div class="modal-backdrop" style="display: none;"></div>

<!-- Fenêtre de la modale -->
<div class="modal-content" style="display: none;">
<h2>Titre de la modale</h2>
<p>Contenu de la modale...</p>
<button onclick="document.querySelector('.modal-backdrop').style.display='none'; document.querySelector('.modal-content').style.display='none'">
Fermer
</button>
</div>

<!-- Conteneur des notifications Toast -->
<div class="toast-container">
<div class="toast">✅ Action réalisée avec succès !</div>
</div>

</body>
Découvrir la solution commentée
/* Architecture de couches sémantiques */
:root {
--z-index-negative: -1;
--z-index-content: 1;
--z-index-navigation: 100;
--z-index-overlay: 500;
--z-index-modal: 510;
--z-index-toast: 600;
}

/* Application des couches */
.background-decoration {
position: absolute;
z-index: var(--z-index-negative);
}

.main-content {
position: relative;
z-index: var(--z-index-content);
}

.main-nav {
position: sticky;
top: 0;
z-index: var(--z-index-navigation);
}

.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: var(--z-index-overlay);
}

.modal-content {
position: fixed;
z-index: var(--z-index-modal);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

.toast-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: var(--z-index-toast);
}