The Quirks of z‑index

Image by Josh Rose.

Z-index is one of those classic examples of easytoforget CSS that nonetheless can frustrate even good developers to no end. It's not uncommon to see elements with z-index declarations like z-index: 9999 !important; on them, even though the layouts in question might not look overly complex.

So, what's up? Why can z-index be such a fiddly and easily misused and misunderstood declaration? It all comes down to a few major quirks that come with the z-index style rule, which start off simple but then become increasingly harder to visualise. Let's see what's going wrong with your styling.


Start with the Element's Stacking Context.

If you are seeing unexpected behaviour, first identify the element's stacking context and the context containing the element it must overlap. z-index applies to positioned elements, flex items and grid items, but its number is compared only within the relevant stacking context.

For an ordinary statically positioned block, z-index has no effect. The important exceptions are flex and grid items, which can use z-index without a nonstatic position. Adding position: relative is therefore not a universal fix; inspect the contexts first.


Context is Key.

Context is where I would say I most commonly see developers struggling with overlaying elements using z-index, and is a far more complex issue. Stacking context is largely affected by your markup and the way elements are grouped and ordered (for now), which makes this a bit more of a pain to resolve.

If you're having some issues making an element appear over other elements, despite applying positioning and sending the z-index declaration to the moon, it's time to check up the DOM tree.

A common example where this kind of issue shows up is modal windows and popups. If the HTML for your modal is outputting into the footer of your site, and your footer is positioned and has a z-index of (for instance) 1, then your modal will appear behind everything on the same level of the DOM tree as the footer, which also has a higher z-index value. If your <main> tag is positioned and has a z-index of 2, your modal will not be able to appear in front of it because, in the context of the footer parent, it is stacked below.

There are a couple of ways to resolve issues like these, but often the best way is to reconsider how your solution is built and why you're needing to have it built this particular way. If there's a reason your footer has a z-index of 1, then changing that just so that the modal works is unlikely to be an option. Generally, the answer is going to lie in restructuring your markup: moving your example modal up the DOM and outside of the footer stacking context so that it can compete with the 2 value applied to <main> directly.

Another way to tackle an issue like this is by removing the position declaration on your footer, or by upping the z-index. Obviously, if there's a reason you had a specific z-index or position declaration on your footer, this isn't the way to go.

So, although convoluted, context is not a hugely complex problem to solve. It can definitely be tricky to root out though don't worry though, we're about to get into the good stuff.


Opacity and Transforms.

Opacity below 1 and a nonnone transform are among the features that create a stacking context. The element and its descendants are then painted as one unit in the parent context, which is why a large descendant z-index cannot escape above a sibling context. The W3C CSS Color specification describes this atomic painting behaviour for opacity.

New stacking contexts can be created by opacity and by transforms? How does that affect the positioning point from above? Well, whilst applying position to an element will resolve many z-index issues, it isn't totally true that you have to have a position declaration in order for z-index to work. Z-index works based on stacking contexts, and doesn't really care how those contexts are created, so if you can create a context without position (hint: you can), then z-index will still work, sometimes quite unexpectedly...

For example:

.box {  height: 20rem;  width: 20rem;  &--green {    background: lime;    z-index: 2;    opacity: 0.9;  }  &--red {    background: red;    z-index: 1;    margin: -10rem 0 0 7.5rem;  }}

Results in this:

Screenshot demonstrating CSS z-index quirks when combined with opacity and negative margins.

In this instance, the opacity on .box--green creates a stacking context. That changes how the green box and its descendants participate in painting; opacity does not itself pull the element upwards, and the result must be read against the complete parent stacking order.


The Wrap‑Up

For something that should be so simple, Z-index can be a deceptively complex declaration, and there are bits and pieces to bear in mind when you're working with it. As long as you get the hang of stacking contexts, you're mostly going to be fine however, getting to grips with those stacking contexts can be surprisingly difficult.

If you're running into a weird issue with stacking, it's worth refreshing yourself with a list of properties that can create stacking contexts.

If you take nothing else away from this article, this I feel is the most important thing to understand about zindices:

Z-index is not a system for defining global layers throughout a document. It orders eligible boxes within a stacking context; a descendant context is painted atomically at its parent's level, regardless of the values used inside it.


Untangling a delivery problem?

Send the symptoms, constraints, and affected routes. I'll help identify whether the issue sits in the application, platform, content model, deployment path, or search surface.