CSS layout reference

Why z-index: 9999 still loses.

Layering bugs are almost never about the number. They are about which group the number is being compared in. This reference covers stacking contexts, what creates them, the order things paint in, and the one-line fix that keeps a component’s layers to itself.

z-index

What a stacking context is

z-index is not a global ranking. It ranks an element against its siblings inside the nearest stacking context — a self-contained group that is flattened and then placed, as one unit, in its parent’s order. A child can never climb out of its group, however large its number. That is the whole reason z-index: 9999 loses.

on the parent
parent
child · z-index: 9999
sibling · z-index: 2

With nothing on the parent, the child’s 9999 competes with the sibling’s 2 and wins. Any other option makes the parent a stacking context: the 9999 now only ranks the child inside the parent, and the parent as a whole sits below the sibling.

Use it for

  • Reading a z-index bug: find the nearest ancestor of each element that forms a context, and compare those two ancestors, not the elements themselves.
  • Predicting overlap between components that were written separately and know nothing about each other’s numbers.

Watch for

  • Raising the number is never the fix when the two elements are in different contexts. The comparison is happening further up the tree.
  • z-index only applies to positioned elements and to flex and grid items. On a static block in normal flow it is ignored.
  • A context is created by many properties that have nothing to do with layering — which is why adding a fade-in animation can suddenly put a dropdown behind the next section.
.card     { position: relative; z-index: 1; }  /* a context: children rank inside it */
.card .menu { z-index: 9999; }                 /* 9999 within .card, never beyond */
.next-card  { position: relative; z-index: 2; } /* paints over .card and its menu */

opacity · transform · filter

What creates a stacking context

The root element, plus any element with: a z-index other than auto while positioned or a flex or grid item; position fixed or sticky; opacity below 1; any transform, filter, backdrop-filter, perspective, clip-path, or mask; mix-blend-mode other than normal; isolation: isolate; will-change naming one of those; or contain: layout or paint. Elements in the top layer form one as well.

Use it for

  • A checklist when a layer misbehaves: walk up from the element and stop at the first ancestor matching any of these.
  • Choosing the least surprising trigger when you want a context on purpose — that is isolation: isolate, which does nothing else.

Watch for

  • Animations create contexts while they run. An element fading in has opacity below 1 for the duration, so layering can change mid-animation and snap back at the end.
  • will-change: transform creates the context permanently, even though nothing is transformed yet.
  • Most of the same properties also make the element the containing block for position: fixed descendants — the trapped-modal bug and the trapped-z-index bug usually arrive together.
  • position: relative alone does not create one. It needs a z-index other than auto.
.fades   { opacity: 0.99; }            /* context */
.lifted  { transform: translateY(-2px); } /* context, and a fixed containing block */
.header  { position: sticky; top: 0; }   /* context, with no z-index at all */
.plain   { position: relative; }         /* not a context */

z-index: auto

Painting order inside a context

Within one context the browser paints back to front in a fixed sequence: the context’s own background and borders; positioned children with negative z-index; in-flow blocks; floats; inline content; positioned children with z-index auto or 0, in source order; then positive z-index, lowest first. Ties always fall back to source order.

z-index: 1positioned, positive
z-index: autopositioned, no z-index
(none)in-flow block
z-index: -1positioned, negative

In the markup these four are written top box first. They still paint back to front in the order negative, in-flow, auto, positive.

Use it for

  • Putting a decorative layer behind an element’s content but above its background: z-index: -1 on a pseudo-element, inside a parent that forms a context.
  • Getting overlap right without any z-index — a positioned element already paints over non-positioned siblings, and later siblings over earlier ones.

Watch for

  • z-index: -1 without a context on the parent sends the layer behind the parent’s own background, and often behind the page. Add isolation: isolate to the parent.
  • z-index: 0 and z-index: auto paint at the same level, but 0 creates a stacking context and auto does not.
  • Text and inline content paint above floats and blocks, so an un-positioned image can sit under the text of a neighbour that overlaps it.
.card         { position: relative; isolation: isolate; }
.card::before { content: ""; position: absolute; inset: 0;
                z-index: -1; /* behind the text, above .card's background */ }

isolation: isolate

Containing a component’s layers

isolation: isolate creates a stacking context and has no other effect: no offset, no containing block, no rendering change. Put it on a component’s root and every z-index inside becomes private. Internals can use 1, 2, 3 without ever colliding with the page, and nothing inside can escape over a sticky header.

Use it for

  • The root of any component with internal layering: cards with badges, sliders with thumbs, image galleries with overlays.
  • The main content wrapper, so that page content as a whole sits below the header and overlays regardless of what it contains.
  • The parent of any z-index: -1 decoration.

Watch for

  • It works in both directions. A dropdown inside an isolated card cannot rise above the next card either — menus and tooltips belong in the top layer, not in a bigger number.
  • It does not clip anything. Isolation is about paint order, overflow is about bounds.
.card { isolation: isolate; }
.card .image   { z-index: 1; }
.card .overlay { z-index: 2; }
.card .badge   { z-index: 3; }   /* private to .card */

--z-*

A z-index scale

Once components isolate their internals, the only z-index values left in the global space are the application’s own layers, and there are few of them. Name them as tokens in one place, in order, with gaps. Anything reaching for a raw number outside a component is then visibly a mistake.

Use it for

  • A short named scale: base, sticky, dropdown, overlay, modal, toast.
  • Code review: a literal z-index in page-level CSS is a prompt to ask which layer it is supposed to be.

Watch for

  • A scale does nothing about contexts. If the header and the modal live in different contexts, their tokens never meet — the scale only orders siblings of the same context, normally direct children of body.
  • Third-party widgets arrive with values like 2147483647. Wrap them in an isolated container rather than outbidding them.
  • Once modals and popovers move to the top layer, most of the scale’s upper end is no longer needed.
:root {
  --z-sticky:   10;
  --z-dropdown: 20;
  --z-overlay:  30;
  --z-modal:    40;
  --z-toast:    50;
}

DevTools

Debugging a layering bug

The question is always the same: which two stacking contexts are actually being compared? Walk up from each element to its nearest context-forming ancestor, then keep going until the two paths share a parent. The two children of that shared parent are the real contestants, and their z-index values — or their source order — decide it.

Use it for

  • In DevTools, step up the ancestors in the Elements panel and check the computed styles of each against the list of context triggers.
  • Toggling a suspect property off in DevTools: if the layering fixes itself, that element was forming the context.
  • Browser 3D or layers views, which draw the stacking contexts as separate planes.

Watch for

  • The culprit is often an innocent-looking wrapper: a transition utility, a will-change, a filter used for a hover effect.
  • “It works until I hover” nearly always means a hover style adds a transform or opacity and creates a context on the fly.
  • If the fix you are about to ship is a bigger number, stop: it will be outbid again. Move the element, isolate the neighbour, or use the top layer.

Start from the symptom

z-index: 9999 and it is still underneath
What a stacking context is
A dropdown went behind the next section after I added an animation
What creates a stacking context
My z-index: -1 background disappeared
Painting order inside a context
A component’s overlay pokes through the sticky header
Containing a component’s layers
Every new layer needs a bigger number than the last
A z-index scale
I cannot tell which ancestor is responsible
Debugging a layering bug

Keep going

Layered content has to fit before it can stack.

The overflow reference covers truncation, line clamping, long words, and the min-width: 0 fix behind most “why won’t it shrink” bugs.

Open the overflow reference

We'd like to use Google cookies to understand how Nodlume is used and to measure our advertising. Nothing loads until you choose, and declining does not affect anything in the app.