Skip to content

Fix: CSS z-index Not Working

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix z-index not working in CSS. Covers stacking contexts, position property requirement, parent stacking context limits, opacity and transform creating new stacking contexts, flexbox and grid z-index, and the isolation property.

The Error

You set a high z-index on an element, expecting it to appear on top of everything else:

.modal {
  z-index: 9999;
}

Nothing happens. The element stays behind other content. You bump it to z-index: 999999. Still behind. You try z-index: 2147483647 (the max 32-bit integer). Still nothing.

The number doesn’t matter. The problem is almost never about the value of z-index — it’s about stacking contexts.

Why This Happens

z-index only works under specific conditions. When those conditions aren’t met, the property is silently ignored — no error, no warning, no DevTools hint.

Two things must be true for z-index to have any effect:

  1. The element must participate in a stacking context. For most elements, this means having a position value other than static (the default). Without it, z-index is ignored entirely.

  2. The element’s stacking context must be the right one. An element can only compete with siblings in the same stacking context. A z-index: 9999 inside a parent with z-index: 1 will never appear above a sibling of that parent with z-index: 2. The parent’s z-index caps everything inside it.

Think of stacking contexts like folders on a desk. You can reorder papers within a folder, but the folder itself has a position in the pile. No matter how you arrange papers inside folder A, they’ll all stay below folder B if folder B is on top.

Version history: how stacking context rules grew

Stacking context is older than most CSS features developers reach for daily, but the list of properties that create one has expanded steadily. The original CSS 2.1 spec only created a stacking context when an element had position other than static combined with a non-auto z-index, or when it was the root element. That short list is the model many older tutorials still teach.

CSS3 and later modules added many more triggers. opacity below 1 was promoted to a stacking-context creator early to make alpha compositing predictable. transform, filter, perspective, clip-path, mask, mix-blend-mode, and will-change were added with the introduction of CSS Transforms, Filters, and Masking specs. position: fixed and position: sticky were made unconditional stacking-context roots — they no longer need a z-index to start one. isolation: isolate was introduced specifically as a side-effect-free way to start a stacking context (it has no visual effect of its own, unlike opacity or transform). Most recently, CSS Containment Level 1 — now widely supported in modern browsers — adds contain: layout, contain: paint, and contain: strict to the list. Setting any of those on an element also creates a new stacking context.

The takeaway: if a z-index problem suddenly appeared after a CSS refactor or a framework upgrade, scan the chain of ancestors for any property in that expanded list. A contain: layout added for performance reasons, a backdrop-filter added for a glass effect, or a will-change: transform added by a third-party animation library can all silently introduce stacking contexts that did not exist a year ago. Modern browsers (Chrome 105+, Firefox 110+, Safari 16+) implement the full list — so a layout that worked on an older browser may also misbehave on a current one for this reason.

Fix

1. Add position to the Element

This is the most common cause. z-index has no effect on elements with position: static (the default).

Broken code:

.tooltip {
  z-index: 100; /* ignored — position is static */
}

Fix — add a position value:

.tooltip {
  position: relative; /* now z-index works */
  z-index: 100;
}

Any of these values will make z-index work: relative, absolute, fixed, or sticky.

Use position: relative when you want the element to stay in the normal document flow but still respond to z-index. It won’t move the element unless you also set top, left, right, or bottom.

2. Check the Parent’s Stacking Context

This is the most misunderstood cause. Your element’s z-index only competes with other elements in the same stacking context. If a parent element creates its own stacking context, your element is trapped inside it.

Broken code:

<div class="sidebar" style="position: relative; z-index: 1;">
  <div class="dropdown" style="position: absolute; z-index: 9999;">
    <!-- This will NEVER appear above .main-content -->
  </div>
</div>

<div class="main-content" style="position: relative; z-index: 2;">
  <!-- This sits above .sidebar and everything inside it -->
</div>

.dropdown has z-index: 9999, but its parent .sidebar has z-index: 1. The browser compares .sidebar (z-index: 1) against .main-content (z-index: 2) at the parent level. .main-content wins. Everything inside .sidebar — including .dropdown — is rendered below .main-content.

Fix — raise the parent’s z-index:

.sidebar {
  position: relative;
  z-index: 3; /* now higher than .main-content */
}

Or remove the parent’s stacking context if it doesn’t need one:

.sidebar {
  position: relative;
  /* remove z-index entirely — no stacking context created */
}

Without a z-index, the .sidebar doesn’t create a stacking context, and .dropdown competes at the root level where its z-index: 9999 works as expected.

Note: You can only remove the parent’s z-index if it doesn’t need to be stacked itself. If both the parent and the child need specific stacking, you may need to restructure your HTML so the child is not nested inside the parent.

Real-world scenario: You add a subtle fade-in animation to a card component using opacity: 0 to opacity: 1. Suddenly, the dropdown menu inside the card stops appearing above the rest of the page — because any opacity value less than 1 creates a new stacking context that traps all children.

3. Remove Properties That Silently Create Stacking Contexts

Several CSS properties create a new stacking context even without z-index being set. This catches people off guard because the property seems unrelated to stacking.

These properties create a new stacking context on an element:

  • opacity with a value less than 1
  • transform with any value other than none
  • filter with any value other than none
  • backdrop-filter with any value other than none
  • perspective with any value other than none
  • clip-path with any value other than none
  • mask / mask-image with any value
  • mix-blend-mode with any value other than normal
  • isolation: isolate
  • will-change when set to a property that would create a stacking context (e.g., will-change: transform)
  • contain with layout, paint, or strict

Broken code — opacity creating a hidden stacking context:

.card {
  opacity: 0.99; /* creates a stacking context */
}

.card .popup {
  position: absolute;
  z-index: 9999; /* trapped inside .card's stacking context */
}

Even opacity: 0.99 — visually identical to opacity: 1 — creates a stacking context. This is a common source of confusion when adding fade-in animations.

Broken code — transform creating a hidden stacking context:

.header {
  transform: translateZ(0); /* "GPU acceleration hack" — creates stacking context */
}

.header .dropdown-menu {
  position: absolute;
  z-index: 1000; /* trapped inside .header */
}

The transform: translateZ(0) trick (sometimes used for GPU acceleration) creates a stacking context on .header, trapping everything inside it.

Fix — remove the property if possible, or restructure:

.header {
  /* Remove transform: translateZ(0); if not needed */
}

If you need the transform for animation purposes, move the dropdown outside the transformed parent in the HTML:

<!-- Before: dropdown trapped inside transformed header -->
<header class="header">
  <nav>...</nav>
  <div class="dropdown-menu">...</div>
</header>

<!-- After: dropdown is a sibling, free from header's stacking context -->
<header class="header">
  <nav>...</nav>
</header>
<div class="dropdown-menu">...</div>

4. Flexbox and Grid Children: z-index Works Without position

Here’s an exception that trips people up in the opposite direction. Flex items and grid items can use z-index without setting position.

.flex-container {
  display: flex;
}

.flex-container .item {
  /* No position needed — z-index works on flex items */
  z-index: 1;
}

This is defined in the spec: flex items and grid items establish a stacking context when z-index is set, even if their position is static.

Where this matters: If you’re debugging z-index on a flex or grid child and you add position: relative thinking it’ll fix the issue — it won’t help because z-index was already working. The problem is likely a parent stacking context (see Fix 2).

5. Use isolation: isolate to Control Stacking Contexts

The isolation property exists specifically to create a stacking context without side effects. Use it when you want to contain z-index stacking within a component without affecting the rest of the page.

.modal-wrapper {
  isolation: isolate;
}

This creates a stacking context on .modal-wrapper without changing its opacity, transform, or position. Elements inside it can use z-index to layer among themselves, and none of them will leak out and interfere with elements outside .modal-wrapper.

Practical example — preventing a component’s internal z-index from leaking:

/* Component styles */
.card {
  isolation: isolate; /* contains z-index stacking */
}

.card .background {
  position: absolute;
  z-index: -1; /* stays behind card content, doesn't fall behind elements outside .card */
}

.card .content {
  position: relative;
  z-index: 0;
}

Without isolation: isolate on .card, the z-index: -1 on .background could fall behind elements outside the card entirely, because it would be competing in the parent’s stacking context.

6. Move the Element Outside Its Current Stacking Context

Sometimes the cleanest fix is structural. If an element needs to appear above everything — like a modal, tooltip, or dropdown — it should live at the top level of the DOM, not nested deep inside a component tree.

Using a portal in React:

import { createPortal } from 'react-dom';

function Modal({ children }) {
  return createPortal(
    <div className="modal-overlay">
      <div className="modal">{children}</div>
    </div>,
    document.body
  );
}

If your portal renders differently on the server and client, you may also encounter hydration mismatch errors. Place the portal target element at the root of the body and ensure both server and client render the same wrapper structure.

Using a top-level element in plain HTML:

<body>
  <div class="app">
    <!-- your app content with its own stacking contexts -->
  </div>

  <!-- modal lives outside the app tree — no stacking context traps it -->
  <div class="modal-overlay">
    <div class="modal">...</div>
  </div>
</body>
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
}

By placing the modal at the root level, its z-index competes at the top of the document, not inside some nested stacking context.

How to Debug z-index Issues

Step 1: Inspect the Element in DevTools

Open your browser’s DevTools, select the element that isn’t stacking correctly, and check:

  1. Does it have position set? If not, z-index won’t work (unless it’s a flex/grid child).
  2. What is its computed z-index value? Check the Computed tab, not just the Styles tab. A rule further down might be overriding your value.

Step 2: Walk Up the DOM Tree

Select each ancestor element and check whether it creates a stacking context. Look for:

  • z-index combined with position other than static
  • opacity less than 1
  • transform, filter, perspective, clip-path set to anything other than none
  • isolation: isolate
  • will-change set to a stacking-related property
  • position: fixed or position: sticky (these always create a stacking context)
  • contain: layout, contain: paint, or contain: strict

Any of these on a parent creates a new stacking context that traps your element.

Step 3: Use the Stacking Context Inspector

Chrome/Edge: Search for “stacking context” extensions in the Chrome Web Store. The “CSS Stacking Context Inspector” extension highlights all stacking contexts on the page and shows their hierarchy.

Firefox: Firefox DevTools show a “stacking context” badge on elements in the inspector when they create one.

Still Not Working?

Check for !important Overrides

Another stylesheet might be overriding your z-index with !important:

/* Some framework or reset CSS you forgot about */
.widget * {
  z-index: 0 !important;
}

In DevTools, look at the Styles panel for your element. Crossed-out z-index declarations mean they’re being overridden. Check all matching selectors.

Check position: fixed Elements

position: fixed elements are positioned relative to the viewport, but they still participate in the stacking context of their nearest ancestor that creates one. This means a position: fixed modal inside a transformed parent won’t behave as expected — it loses its viewport-relative positioning and gets trapped in the parent’s stacking context.

.animated-page {
  transform: translateX(0); /* creates stacking context */
}

.animated-page .modal {
  position: fixed; /* broken — no longer relative to viewport */
  z-index: 9999;   /* trapped inside .animated-page's stacking context */
}

Fix: Move the fixed element outside the transformed parent in the DOM (see Fix 6).

Negative z-index Falling Behind the Background

If you set z-index: -1 on an element to place it behind its parent’s content, it might disappear behind the parent entirely:

.parent {
  background: white;
}

.parent .behind {
  position: absolute;
  z-index: -1; /* falls behind .parent's background */
}

The element with z-index: -1 is rendered below the parent’s background because the parent doesn’t create a stacking context. The element drops down to the parent’s parent context.

Fix — create a stacking context on the parent:

.parent {
  background: white;
  isolation: isolate; /* or position: relative; z-index: 0; */
}

.parent .behind {
  position: absolute;
  z-index: -1; /* now behind content but above parent's background */
}

Animations and Transitions Temporarily Breaking z-index

CSS animations and transitions that involve transform, opacity, or filter create stacking contexts only while running. This means z-index stacking can change mid-animation, causing elements to visually “jump” between layers.

If a dropdown works fine until a nearby element starts animating, the animation is temporarily creating a stacking context that interferes. Add isolation: isolate to the animated element’s parent to contain the stacking context, or use will-change to make the stacking context permanent (so the layout stays consistent):

.animated-element {
  will-change: transform; /* permanent stacking context — no visual jump */
}

Warning: Don’t apply will-change to too many elements. It consumes GPU memory. Only use it on elements that actually animate. If your CSS or JS bundle is failing to load entirely, the z-index issue might be secondary to a failed chunk load or an unresolved Vite import failure preventing the stylesheet from being applied.

position: sticky always creates a stacking context

Once position: sticky is set, the element is automatically a stacking-context root — even with no z-index declared. If a sticky header swallows children that need to escape its layer, the cause is the implicit stacking context, not a missing z-index. Move the escaping element outside the sticky parent or accept that its top z-index is bounded by the sticky element’s place in the parent context. For related layout failures, see Fix: CSS position sticky not working.

Flexbox or grid item z-index ignored under Tailwind

If you set z-index on a flex or grid child via a Tailwind class (z-10, z-50) and nothing happens, two things may be wrong simultaneously. First, the parent may have an existing stacking context that caps the child. Second, Tailwind’s reset may not include the z- utility you expect — pre-v4 versions only generated a small set by default (z-0, z-10, z-20, z-30, z-40, z-50, z-auto). Arbitrary values like z-[9999] require the JIT engine or Tailwind v3+. If the class is not generated, the rule is silently absent from the compiled stylesheet. Confirm in DevTools that the class actually appears. For broader class-output issues, see Fix: Tailwind classes not applying.

Stacking context inside <dialog> and the top layer

Native <dialog open> elements rendered via dialog.showModal() are promoted to the browser’s “top layer” — a special render layer above every stacking context in the page. A modal opened this way will always appear above your z-index: 9999 content, which can be useful or surprising. The reverse is also true: if your design depends on a tooltip appearing above a <dialog>, you must render the tooltip inside the same dialog (or also in the top layer via another showModal()). The top layer was standardized for modern browsers in 2023; older code that used Bootstrap-style modal divs does not benefit from it.


Related: Fix: CSS position sticky not working | Fix: Tailwind classes not applying

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles