Web Platform · CSS Layout

Positioning and Stacking Contexts

How out-of-flow boxes resolve their offsets, which ancestor supplies their containing block, and how z-index orders boxes through nested stacking contexts.

z-index: 9999 failing to raise an element above a sibling is not a browser bug. It is the expected result when the two elements live in different stacking contexts: an element’s z-index orders it among its siblings inside its nearest stacking context, and no value can lift it out of that context. The same structural rule governs absolute positioning, where an offset is resolved against an ancestor that may not be the parent.

Both properties describe participation in a tree of boxes rather than a flat document. The useful mental model is that certain boxes create a new local coordinate system and a new local ordering space, and descendants are confined to it until they create one of their own.

Taking a box out of flow

The position property has five values. static is the initial value and means the box is laid out by normal flow with no offsets. relative keeps the box in flow and offsets its painted position by the inset properties, leaving its original space reserved. absolute removes the box from flow entirely and positions it against a containing block selected by ancestry. fixed does the same against the viewport. sticky is a relative box that is clamped to the scrollport within a range defined by its nearest scrolling ancestor.

Removal from flow means the box no longer contributes to the block size of its parent and no longer interacts with sibling margins. An absolutely positioned box with top: 0; left: 0 sits at the top-left of its containing block regardless of where it would have appeared in the document. Siblings close up as if it were absent, which is why an absolutely positioned element can overlap the text that follows it unless space is reserved explicitly.

For an absolutely positioned non-replaced box, the inset properties and the size solve an over-constrained equation similar to the one described for block widths in Used Values and the Containing Block. If left and right are both specified and width is auto, the box stretches between them. If left is specified, width is specified, and right is auto, the box is placed at left and the right inset is ignored. If all three are auto, the box is placed at its static position, which is where it would have been in normal flow.

position: sticky is the least intuitive of the values. The box is first laid out in flow, then offset to remain within the scrollport as long as its containing block is in view. A sticky header inside a scrolling container sticks while that container’s content scrolls past and is released when the container’s edge arrives. Stickiness does not work if any ancestor between the element and the scroll container has overflow: hidden or overflow: auto, because that ancestor becomes the scroll container and the element sticks to it instead.

Selecting the containing block

For an absolutely positioned box, the containing block is the padding box of the nearest ancestor whose position is not static. That ancestor is a “positioned” ancestor, and position: relative with no offsets is the usual way to create one. The containing block edges are the ancestor’s padding edges, so inset: 0 makes the child’s margin box coincide with the padding box, not the border box.

Fixed boxes use the viewport, except when an ancestor creates a containing block for fixed descendants. A transform, a filter, perspective, contain: paint, contain: layout, or will-change naming one of those properties on an ancestor does this. The effect is easy to observe: a modal with position: fixed; inset: 0 placed inside an animated card gets confined to the card’s bounds because the card’s transform establishes a containing block.

The same transformed ancestor also becomes the reference for position: absolute descendants and, in many engines, changes how scrolling interacts with fixed content. The design consequence is that animation and containment properties are not purely visual; they alter the geometry of every descendant that depends on an ancestor’s box.

Stacking contexts

A stacking context is an independent ordering space. Boxes inside it are painted in a defined order relative to each other; the entire context is then painted as one unit within its parent context, using the parent’s z-index. The contexts form a tree that mirrors the box tree.

The following boxes establish a stacking context: the root element; a positioned box with a z-index other than auto; a fixed or sticky box, even with z-index: auto; a flex or grid item with a z-index other than auto; an element with opacity less than 1; an element with a non-none transform, filter, perspective, clip-path, mask, or backdrop-filter; an element with isolation: isolate; an element with mix-blend-mode other than normal; and an element with contain: paint or contain: layout.

Inside a stacking context, the painting order follows the sequence in CSS 2.1 Appendix E. From back to front:

  1. The background and border of the element that establishes the context.
  2. Child stacking contexts with negative z-index, most negative first.
  3. In-flow, non-positioned block-level descendants, in tree order.
  4. Non-positioned floats, in tree order.
  5. In-flow inline-level descendants, including the inline content of blocks.
  6. Positioned descendants with z-index: auto or z-index: 0, and child stacking contexts with z-index: 0.
  7. Child stacking contexts with positive z-index, smallest first.

Step 1 matters for a common bug: a child with z-index: -1 paints behind its parent’s background if the parent establishes the stacking context, so the child becomes invisible when the parent has an opaque background. The child is in front of the parent’s ancestors’ backgrounds but behind the parent’s own decoration.

How z-index resolves

z-index accepts an integer or auto, alongside the CSS-wide keywords that every property takes. auto means the box does not establish a stacking context by itself and participates at the level appropriate to its position in the painting order. An integer establishes a context and places the whole context at that level among its siblings.

The comparison is always among siblings within one context. Two boxes with z-index: 1 and z-index: 2 in the same context order by number. A descendant with z-index: 100 inside a context placed at z-index: 1 cannot paint above a sibling context at z-index: 2, because the descendant’s number is compared only inside its own context.

The integer range is not bounded by the specification, and implementations use a fixed-width integer internally. Values in the millions work, but they do not travel across contexts and they invite an escalation that cannot solve a structural problem.

/* The header is a stacking context at z-index 1. */
.header { position: sticky; top: 0; z-index: 1; }

/* The badge inside the header is ordered only against the header's
   other children; its 999 cannot escape the header's level. */
.header .badge { position: absolute; z-index: 999; }

/* Overlay sits in the root context and wins over the header. */
.overlay { position: fixed; inset: 0; z-index: 2; }

Failures that look like z-index failures

Several properties create contexts unexpectedly, and each produces the same symptom: a high z-index appears ignored.

Opacity. Any opacity below 1 creates a context, including opacity: 0.999. A fade-in transition on a dropdown therefore isolates everything inside it for the duration of the animation, and stacking can change at the start and end of the transition.

Transforms and filters. Any transform or filter value other than none creates a context, even one that leaves the box exactly where it was, so transform: translateZ(0) and filter: blur(0) are enough. Hover effects that apply a transform are therefore a frequent cause of a tooltip losing its place in the order.

Containment. contain: paint clips descendants and creates a stacking context, and content-visibility: auto implies contain: layout style paint. The clipping is the more visible effect, but the ordering change is real.

Parent backgrounds. A negative z-index child is painted behind its stacking-context-establishing parent’s background, which hides it. The fix is to move the negative child into a context where the background is not between them, not to raise the number.

Isolation. isolation: isolate creates a context without any visual effect of its own. It is the explicit tool for containing mix-blend-mode and for preventing a child from blending with content outside its container.

The interactions between positioning, overflow, and paint order become harder to reason about when boxes are clipped or scrolled, which is why the fragmentation and containment behaviours described in Containment and Container Queries treat paint containment as a layout decision rather than a rendering hint.

References

  1. CSS Positioned Layout Module Level 3 — W3C
  2. CSS 2.1, Appendix E: Elaborated description of the meaning of stacking contexts — W3C
  3. CSS Color Module Level 3 (opacity) — W3C