Z-Indexes in CSS Explained

Z-Index is one of the most confusing and unintuitive properties in CSS, but it’s actually pretty simple once you understand it.

Your first instinct is probably to think that you can just set z-index on any element and that that alone is going to determine its stacking order. This is WRONG.

First of all: z-index only applies to positioned elements.

A positioned element is an element who’s position property is NOT static (eg. relative, absolute, fixed). Setting a z-index on an unpositioned element does nothing.

If a positioned element has a z-index of X, then all of its children will also be stuck with a z-value of X

You can change the z-index of any child elements all you want, but that won’t do anything!

(Note: z-value is not an actual CSS term. I only use it because z-index technically defaults to “auto” and can be set to whatever you want, even if it does nothing)

<div id="parent" style="position: relative; z-index: 6;">
<div id="child" style="position: relative: z-index: 7;">
<div id="grandchild" style="position: relative; z-index: 9;" >
</div>
</div>
</div>

The z-value of each of these elements is 6, and there’s no way to change that without modifying the z-index of #parent.

In technical terms, a stacking context is formed when a positioned element has a z-index. A stacking context is a single atomic unit composed of a parent along with its children. All elements within a stacking context are bound to the z-value set by the non-root parent (in this case #parent).

(Non-root just means that it’s not the <html> tag. The root <html> tag technically forms a stacking context, but any z-index on it is meaningless.)

So you can create stacking contexts within stacking contexts, but all child stacking contexts will be bound to the z-value of the outermost (non-root) stacking context.

In the example above, three stacking contexts are formed, and they’re nested. The z-value of all three contexts is bound to the z-index of #parent, which is 6.

Stacking Contexts:
#parent (z-value: 6)
#child
#grandchild

Turns out that setting the z-index on a positioned element is only one of many ways to create a stacking context. There are various CSS properties that automatically create a stacking context on an element, regardless of whether or not a z-index is set.

The full list is here, but here are some common properties that automatically create new stacking contexts:

  • position: fixed
  • elements with a transform value other than "none"
  • elements with an opacity value less than 1.

So any element with position: fixed or transform: translateY(50%) is going to form a new stacking context regardless of whether a z-index has been set on that element.

Some implications of this:

  • If you must use fixed positioning, make the element as shallow as possible so that it doesn’t accidentally get thrown under another stacking context and bound to that element’s z-index.
  • Avoid using transform to center containers/wrappers if possible (commonly used for vertically centering). The entire container will be bound to the same stacking context, and thus you won’t be able to individually modify the z-index values of any of its children.
  • Use background-color with an rgba()value instead of opacity. This avoids the creation of a stacking context.

If any of this didn’t make sense, just read MDN’s guide. It’s pretty good.

Now that you understand z-indexes and stacking contexts, you know something 95% of developers forced to write CSS don’t know, and will no longer have to suffer through migraines figuring out why that z-index isn’t working.