Why does z-index not work on an element even though you have set a high value?

Intermediate12 min interview
Skills tested:
Checking that the element is positioned or is a flex/grid itemLocating an ancestor stacking context that scopes the z-indexDistinguishing a stacking problem from an overflow clipping problemRecognising that siblings must share a context to be comparedDebugging with devtools rather than escalating the value

Advertisement

🧩 Scenario

This is one of the most common real debugging tasks in frontend work, and the instinct almost everyone has is wrong. The value gets bumped from 10 to 100 to 9999, the element still renders underneath, and eventually someone concludes CSS is broken. The productive path is a short checklist: is the element positioned, does an ancestor create a stacking context, is this actually clipping rather than stacking, and are the two elements even in the same context to begin with.

Architecture Walkthrough

Reason 1: The Element Is Not Positioned

z-index applies only to positioned elements, meaning position is relative, absolute, fixed, or sticky. On a position: static element, which is the default, z-index is parsed and then completely ignored.

There are exactly two exceptions, both explicitly carved out by their specs: flex items and grid items honour z-index without any position value. So z-index on a direct child of a display: flex or display: grid container works, while the same declaration on a child of a plain block container does nothing.

This is the first thing to check, and it accounts for a surprising share of cases, particularly in codebases where a position: relative was removed during a refactor.

Reason 2: An Ancestor Created a Stacking Context

This is the reason behind almost every case that survives the first check, and it is why raising the value never helps.

z-index is scoped to the nearest ancestor stacking context. If that ancestor is itself painted below another element, nothing inside it can rise above that element. A z-index: 9999 child inside a context that paints at level 1 loses to a z-index: 2 element in the parent context, every time.

What makes this hard to spot is that the context is usually created by a property nobody added for stacking reasons. opacity below 1 from a fade animation, a transform from a hover lift, a filter for a drop shadow, a will-change hint added for performance, contain: paint, or backdrop-filter on a glassy panel: any of those creates a stacking context on the spot. The CSS on the failing element looks perfect; the cause is three levels up in a rule that mentions nothing about layering.

The fix is always to raise the ancestor that created the context, not the descendant.

Reason 3: It Is Clipping, Not Stacking

If the element is being cut off at an ancestor's boundary rather than rendered behind something, this is not a z-index problem at all and no z-index value will fix it.

An ancestor with overflow: hidden, auto, scroll, or clip clips its descendants to its padding box. A dropdown or tooltip that needs to extend past its container gets truncated regardless of stacking order. The same happens with contain: paint and with clip-path.

Telling the two apart is straightforward: a stacking problem shows the whole element, just underneath something else. A clipping problem shows part of the element with a hard straight edge exactly at an ancestor's boundary. The fixes are entirely different too: clipping is solved by removing the overflow, by portalling the element out of the clipping ancestor, or by using the native top layer via <dialog> or the Popover API, which escapes both clipping and stacking.

Reason 4: The Two Elements Are Not in the Same Context

Comparing z-index values across different stacking contexts is meaningless. Two elements can only be ordered against each other by z-index if they are siblings in the same context. If they live in different contexts, the ordering is decided by where those contexts sit relative to each other, and the descendants' own values are irrelevant to that comparison.

Reason 5: The Value Is Not Actually Applying

The mundane explanations are worth ruling out too. The declaration may be overridden by a more specific rule, a later rule, an !important, or a rule in a later cascade layer. It may be inside a media query that does not currently match. Or the element may be painting in the right order already and the real issue is a negative z-index elsewhere, or a positioned sibling later in the DOM that legitimately paints on top because both have z-index: auto and source order decides.

The Debugging Workflow

Rather than guessing, use devtools. Select the element and check the computed position first. Then walk up the ancestor chain in the elements panel, checking each ancestor's computed transform, opacity, filter, will-change, contain, and isolation. Chrome's Layers panel and the "stacking context" badge shown in the elements tree make the offending ancestor visible directly. Once you have found it, decide whether the ancestor's own z-index should change, or whether the context-creating property can be removed, made conditional on interaction, or replaced with isolation: isolate scoped deliberately.


Key Code Explained

/* Reason 1: not positioned -> z-index is parsed and ignored */
.broken {
  z-index: 500; /* no effect: position is static */
}
.fixed-1 {
  position: relative;
  z-index: 500; /* now it applies */
}
/* The two exceptions: flex and grid items need no position */
.flex-parent > .item {
  z-index: 2; /* works, parent is display: flex */
}

/* Reason 2: an ancestor stacking context caps the child */
.card {
  transform: translateY(0); /* stacking context, z-index: auto */
}
.card .tooltip {
  position: absolute;
  z-index: 9999; /* scoped to .card; cannot beat .sibling below */
}
.sibling {
  position: relative;
  z-index: 1;
}
/* Fix: raise the CONTEXT CREATOR, not the descendant */
.card:hover,
.card:focus-within {
  position: relative;
  z-index: 10;
}

/* Reason 3: clipping, not stacking. No z-index value fixes this. */
.container {
  overflow: hidden; /* clips the dropdown at the padding box */
}
.container .dropdown {
  position: absolute;
  top: 100%;
  z-index: 9999; /* irrelevant: the element is cut off, not covered */
}
/* Fixes: remove overflow, portal the dropdown out, or use the top layer */

/* Reason 4: values in different contexts are not comparable */
.ctx-a {
  position: relative;
  z-index: 1;
}
.ctx-a .child {
  position: relative;
  z-index: 100; /* only ranks against siblings inside .ctx-a */
}
.ctx-b {
  position: relative;
  z-index: 2; /* .ctx-b's entire subtree paints above .ctx-a's */
}

/* Reason 5: source order already decides when both are auto */
.first,
.second {
  position: absolute; /* z-index: auto -> .second paints on top */
}

The .card / .sibling block is the canonical failure and the one to be able to explain unprompted. The dropdown's own CSS is flawless. The transform on .card was almost certainly added for an animation, has nothing to do with layering, and yet it is the entire cause. That is why the answer to "z-index is not working" is a question about ancestors rather than about the element.

The .container block matters for a different reason: it is the case where the whole z-index line of investigation is a dead end. Recognising clipping by its hard straight edge saves the time otherwise spent escalating a value that was never going to matter.


Tradeoffs

SymptomReal causeFix
z-index has no effect at allElement is staticAdd position: relative, or make it a flex/grid item
Element renders fully but underneathAncestor stacking contextRaise the ancestor's z-index or remove its context-creating property
Element is cut off with a hard edgeoverflow, contain: paint, or clip-pathRemove overflow, portal out, or use the top layer
Values seem to be compared wronglyDifferent stacking contextsCompare the contexts, not the descendants
Value never appears in computed stylesCascade override or unmatched media queryInspect the cascade in devtools

What Interviewers Actually Check

  • Whether you check for a position value before anything else, and know the flex/grid exceptions
  • Whether you go looking for an ancestor stacking context instead of raising the number
  • Whether you can distinguish clipping from stacking by the visual symptom
  • Whether you know values in different contexts are not comparable
  • Whether you can describe a concrete devtools workflow rather than trial and error

Follow-Up Questions

  1. How do the native top layer used by <dialog> and the Popover API avoid both stacking context and overflow clipping issues?
  2. If you must keep an ancestor's overflow: hidden, what are your options for rendering a dropdown outside it?
  3. Why does adding will-change: transform for performance sometimes break existing layering?
  4. How would you set up a z-index scale in a design system so that modals, toasts, and dropdowns never conflict?
  5. When is isolation: isolate the right deliberate response to a stacking bug rather than a workaround?

Common Candidate Mistakes

  • Setting z-index on a position: static element and concluding the browser is at fault, without knowing that only positioned elements plus flex and grid items honour it
  • Escalating from 10 to 100 to 9999 rather than inspecting the ancestor chain, which never fixes a stacking context problem and leaves an unmaintainable value behind
  • Treating an element that is cut off at a container boundary as a stacking problem, when the hard straight edge indicates overflow clipping that no z-index can address
  • Comparing z-index values on two elements that live in different stacking contexts, where the descendants' values have no bearing on the outcome
  • Overlooking that two positioned siblings with z-index: auto are ordered by source order, so the later one legitimately paints on top with no z-index involved

Interview Readiness Checklist

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

  • Can you list the five main reasons a z-index appears to be ignored?
  • Can you name the property that must be set for z-index to apply, plus the two exceptions?
  • Can you explain how an ancestor's transform breaks a child's z-index?
  • Can you tell a clipping bug from a stacking bug by inspecting the rendering?
  • Can you describe a devtools workflow for finding the offending ancestor?

Summary

A z-index that appears to do nothing almost always has one of five explanations. The element may not be positioned, since z-index applies only to relative, absolute, fixed, and sticky elements, with flex and grid items as the two spec-level exceptions. An ancestor may have created a stacking context, scoping the child's value so that no number can lift it above content outside that context. The problem may not be stacking at all but overflow clipping, which produces a hard straight cut at an ancestor's boundary and is immune to z-index. The two elements being compared may live in different contexts, making their values incomparable. Or the declaration may simply be losing in the cascade.

The ancestor stacking context case is the one that generates the most wasted effort, because the context is usually created by a property added for an unrelated reason: an opacity fade, a transform hover lift, a filter shadow, a will-change hint, or backdrop-filter on a glass panel. The failing element's CSS looks correct because it is correct; the cause is upstream.

The reliable workflow is a checklist rather than escalation. Confirm the computed position, walk up the ancestor chain checking transform, opacity, filter, contain, will-change, and isolation, and use the stacking context badges in devtools to find the creator. Then fix the ancestor. When an element genuinely must escape both clipping and stacking, the native top layer via <dialog> or the Popover API is the structural answer rather than a larger number.

Frequently Asked Questions

Is there a maximum z-index value?

It is a 32-bit signed integer, so the practical ceiling is 2147483647. Values beyond that are clamped, but hitting the ceiling is always a symptom of a stacking context problem rather than a real need.

Does z-index work on flex and grid items without position?

Yes. Flex and grid items are explicitly opted in by their specs, so z-index applies to them with no position value set.

Advertisement


Stay Updated

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

Advertisement