CSS stacking context: why your z-index does nothing

I thought I had seen every way a header could break until I had to debug a dropdown that kept hiding behind a hero image, with a z-index of 999,999 on it. That particular frustration almost always means a CSS Stacking Context problem. Adding more nines will not move the element, because the number was never the thing that was wrong.

After 14 years of working on WordPress themes, the point I wish someone had made to me earlier is that z-index is not a global ranking. It is local. Picture the page as a desk and the elements as sheets of paper, stacked in the order they appear in the HTML. Certain CSS properties gather a group of those sheets into a folder. Inside the folder, z-index orders the sheets in that folder and nothing else. A sheet cannot climb out to sit above paper in another folder. The whole folder has to move.

Common triggers for a new CSS stacking context

Most developers know that position: relative (or absolute, or fixed) plus a z-index makes a new context. Modern CSS creates these folders in far less obvious places, usually while you are optimizing for better performance or adding a bit of visual polish.

These are the properties that tend to start a new CSS Stacking Context:

  • opacity below 1.
  • transform set to anything other than none, such as scale or translate.
  • Any filter value, which is how the blurry background effects catch you out.
  • will-change, when it names a property that would create a context.
  • contain: layout or contain: paint.

One client’s mobile menu refused to overlap the content. The cause was opacity: 0.99 on a parent container, left over from a fade-in animation. That single line flattened the menu’s z-index and dropped it below everything else on the page.

The trapped modal

Here is the version of this I run into most. A header, a main content area, and the modal markup sitting inside the header because that is where the trigger button lives. It reads as reasonable, and it breaks.

/* The Bad Code */
.site-header {
    position: relative;
    z-index: 1;
    opacity: 0.95; /* Trigger! New stacking context created. */
}

.modal-overlay {
    position: fixed;
    z-index: 9999; /* This number is now useless outside the header. */
}

.main-content {
    position: relative;
    z-index: 2; /* This will always cover the modal. */
}

.site-header has a z-index of 1 and a context of its own, so the browser weighs it against .main-content at 2. Main content wins, and the modal stays stuck inside the header’s folder. This is also why WordPress core updates can sometimes break styles: one property change in a parent block resets the stacking hierarchy for everything underneath it.

How to regain control

Which fix I reach for depends on how much of the legacy code I am allowed to touch.

1. Restructure the DOM

The cleanest fix is moving modals and overlays to the end of the <body>. In WordPress that means Portals if you are inside a React-based block, or a hook on wp_footer to print the modal markup. Out of the nested parents, the modal lands in the root CSS Stacking Context and behaves.

2. Use the isolation property

When the HTML cannot move, reach for the isolation property. Applying isolation: isolate to a container creates a new context without needing a z-index or a position. That is what you want when a decorative background needs a negative z-index and you would rather it did not vanish behind the page background.

/* The Pro Approach */
.card-component {
    isolation: isolate; /* Creates context cleanly */
}

.card-decoration {
    z-index: -1; /* Sits behind text, but stays inside the card! */
}

The isolation property on MDN has the technical detail. It is a lot cleaner than the transform: translateZ(0) hack we all leaned on before it.

If this CSS stacking context work is eating your dev hours, I can take it over. I have been working with WordPress since the 4.x days.

Debug the parent, not the number

When an element will not stack where you want it, the useful move is to stop editing the z-index and go find the folder it is sitting in. Open DevTools and read the computed properties on each parent up the tree. Usually an opacity, transform or filter is what is holding your element down.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.