What is a stacking context, and what properties create one?

Intermediate12 min interview
Skills tested:
Defining a stacking context as a local z-index scopeListing the main properties that create one, including the non-obvious onesExplaining why a child z-index cannot escape its parent contextReciting the paint order within a stacking contextDiagnosing an overlay bug caused by an unintended stacking context

Advertisement

🧩 Scenario

Stacking contexts are the reason a dropdown menu inside an animated card renders behind the card next to it, no matter how high you push its z-index. The animation set opacity or transform on the card, which quietly created a stacking context, and every z-index inside that card is now scoped to it. Understanding this is what turns an unfixable-looking overlay bug into a two-minute inspection of the ancestor chain.

Architecture Walkthrough

A Local Scope for z-index

A stacking context is a self-contained scope for stacking order along the z-axis. Every element inside it is stacked relative to its siblings within that context only, and the whole context is then stacked as a single atomic unit inside its parent context.

The consequence people find counterintuitive is that z-index is not a global scale. A value of 9999 means "highest among my siblings in my context," not "highest on the page." If the context that element lives in is itself painted below another context, nothing inside it can rise above that other context's content. There is no escape hatch: a descendant cannot punch out of its ancestor's stacking context.

The root element <html> establishes the root stacking context, and every other one nests inside it, forming a tree that parallels but does not match the DOM tree, since only some elements create contexts.

What Creates One

The list is longer than most people expect, and the non-positioning entries are where the bugs come from.

The positioning cases are: position: relative or absolute with a z-index other than auto; position: fixed or sticky, which always create one regardless of z-index.

The cases with no positioning involved at all are the important ones:

  • opacity less than 1
  • transform, scale, rotate, or translate set to anything other than none
  • filter or backdrop-filter other than none
  • perspective or clip-path or mask other than none
  • mix-blend-mode other than normal
  • isolation: isolate
  • contain: layout, contain: paint, or content-visibility values that imply them
  • will-change naming any property that would itself create one
  • a flex or grid item with a z-index other than auto, even though it is not positioned

That last point is worth noting separately: flex and grid children are the one case where z-index works without any position value, because the flex and grid specs explicitly opt them in.

isolation: isolate deserves a mention as the deliberate tool in this list. It creates a stacking context and does nothing else, no opacity change, no transform, no compositing side effect, which makes it the correct way to intentionally scope z-index inside a component.

The Paint Order Inside a Context

Within a single stacking context, the browser paints in this order, from back to front:

  1. The background and borders of the element that established the context
  2. Descendants with negative z-index, in ascending order
  3. In-flow, non-positioned, block-level descendants
  4. Non-positioned floated descendants
  5. In-flow inline-level descendants, including inline text
  6. Positioned descendants with z-index: auto or z-index: 0
  7. Descendants with positive z-index, in ascending order

Two useful facts fall out of that list. A negative z-index places an element behind its own parent's in-flow content but still in front of the parent's background, which is how a decorative pseudo-element sits behind text without hiding the card colour. And inline text paints above floats and non-positioned blocks, which is why text wraps visibly over a float's background rather than under it.

Debugging Approach

When stacking looks broken, the fix is almost never on the element you are looking at. Walk up its ancestor chain and find the nearest element that created a stacking context, typically by way of opacity, transform, or filter applied for an animation or a hover effect. That ancestor is the one whose z-index needs to change, or whose context-creating property needs to be removed or made conditional.


Key Code Explained

/* Positioned + z-index other than auto -> creates a context */
.card {
  position: relative;
  z-index: 1;
}

/* position: relative ALONE does NOT create one */
.anchor-only {
  position: relative; /* z-index stays auto -> no stacking context */
}

/* No positioning needed: each of these creates a stacking context */
.animated {
  opacity: 0.99;
}
.transformed {
  transform: translateZ(0);
}
.blurred {
  filter: blur(0);
}
.hinted {
  will-change: transform;
}
.contained {
  contain: paint;
}

/* The deliberate, side-effect-free way to scope z-index */
.component {
  isolation: isolate;
}

/* THE CLASSIC BUG ------------------------------------------------- */
.card-a {
  transform: translateY(0); /* creates a stacking context, z-index auto */
}
.card-a .dropdown {
  position: absolute;
  z-index: 9999; /* scoped to .card-a — cannot escape it */
}
.card-b {
  position: relative;
  z-index: 1; /* .card-b's whole subtree paints above .card-a's */
}
/* Result: the 9999 dropdown renders BEHIND .card-b.
   Fix: raise .card-a (the context creator), not the dropdown. */
.card-a:hover,
.card-a:focus-within {
  position: relative;
  z-index: 10;
}

/* Negative z-index: behind the parent's content, in front of its background */
.card::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: url('texture.png');
}

/* Flex and grid items honour z-index with NO position value */
.grid > .overlap {
  z-index: 2;
}

The .card-a block is the example to be able to reproduce from memory. Everything about it looks correct in isolation: the dropdown is absolutely positioned with an enormous z-index, and the sibling card has a modest z-index: 1. The only reason it fails is a transform on .card-a that nobody added for stacking reasons at all, probably a hover lift or an entrance animation. That is why "raise the z-index" is the wrong instinct and "find the ancestor context" is the right one.

The isolation: isolate declaration is the other detail worth carrying. Developers routinely write transform: translateZ(0) or opacity: 0.999 to force a stacking context, both of which have real side effects on compositing and rendering. isolation does exactly that one job and nothing else.


Tradeoffs

Way to create a stacking contextSide effectsGood choice?
isolation: isolateNoneYes, the purpose-built option
position: relative + z-index: 0Becomes a positioned ancestor tooYes, common and clear
opacity: 0.999Forces compositing, subtle colour changeNo, a hack
transform: translateZ(0)Promotes to its own layer, extra memoryOnly when you also want compositing
contain: paintClips overflow, blocks descendants escapingOnly when containment is wanted
will-change: transformPersistent layer promotion, memory costNo, not for this purpose

What Interviewers Actually Check

  • Whether you can define a stacking context as a local scope rather than describing z-index as global
  • Whether you know position: relative alone is not enough and z-index must not be auto
  • Whether you can name the non-positioning creators, especially opacity, transform, and filter
  • Whether you know the fix for a nesting bug is on the ancestor, not the element
  • Whether you know flex and grid items honour z-index without being positioned

Follow-Up Questions

  1. Why does a negative z-index place an element behind its parent's content but still in front of the parent's background?
  2. How does mix-blend-mode interact with stacking contexts, and why does isolation: isolate exist partly to control it?
  3. Do stacking contexts and compositing layers refer to the same thing? Where do they diverge?
  4. How would you architect z-index in a design system so that modals, toasts, and dropdowns never fight, given that contexts can appear anywhere?
  5. How does the top layer used by <dialog> and the Popover API sidestep stacking contexts entirely?

Common Candidate Mistakes

  • Treating z-index as one global scale for the whole document, which makes nested stacking bugs look like browser defects
  • Assuming position: relative by itself creates a stacking context, when z-index must also be something other than auto
  • Not knowing that opacity, transform, and filter create contexts with no positioning involved, which is the origin of most real-world overlay bugs
  • Escalating the child's z-index to ever larger numbers instead of finding and raising the ancestor that created the context
  • Forgetting that flex and grid items can use z-index directly, and adding unnecessary position: relative to make it work

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you define a stacking context in one sentence?
  • Can you name at least six properties that create one, including several with no positioning?
  • Can you explain how a z-index: 9999 element can paint below a z-index: 1 element?
  • Can you recite the paint order layers inside a single context?
  • Can you debug a stacking bug by locating the ancestor that created the context?

Summary

A stacking context is a local scope for z-axis ordering. Elements inside it stack relative to their siblings within that context, and the entire context is then painted as one atomic unit in its parent context. Because of that nesting, z-index is never global: a descendant cannot paint above anything outside its ancestor's context, no matter how large its value.

Contexts are created by positioned elements with a z-index other than auto, by fixed and sticky unconditionally, and, crucially, by a long list of properties that involve no positioning at all: opacity below 1, any transform, filter, backdrop-filter, clip-path, mask, mix-blend-mode, isolation: isolate, contain: layout or paint, and will-change on any of those. Flex and grid items also honour z-index without being positioned. isolation: isolate is the purpose-built option when you want a context and nothing else.

Inside a context, painting runs from the context element's own background, through negative z-index descendants, in-flow blocks, floats, inline content, positioned auto/0 elements, and finally positive z-index in ascending order. When stacking looks broken, the productive move is not raising the child's z-index but walking up the ancestor chain to find the element whose transform or opacity created the context, and fixing the ordering there.

Frequently Asked Questions

Does every positioned element create a stacking context?

No. A positioned element creates one only when z-index is not auto. position: relative with z-index: auto does not create a stacking context.

Can a child escape its parent stacking context?

No. A child can never paint above or below anything outside its parent stacking context. The parent whole subtree is stacked as one unit at the parent level.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement