The z-index: of CSS can be very confusing…

You set it to a new value and nothing happens. No matter what value you choose, it seems to have no effect. You try larger and larger z-index: values in a vain attempt to make your element appear on top.

“Why is it not working?” I hear you scream…

But do not despair!

z-index is not that difficult to understand once you understand its core concepts.

This blog post is a short dive into how it works.

What is z-index:?

The z-index: is a property of HTML elements, and is used to control the stacking order of those elements.

It determines the position of an element on the ‘z-axis’, which can be thought of as the order in which elements appear in front of or behind each other.

You can also think of it as the position of the element coming out of the screen towards the viewer.

CSS z-index: it is the position of the element in the z-axis

The CSS z-index: property sets the position of an element on the ‘z-axis’.

HTML elements can overlap

Imagine two div HTML elements on a page, A and B.

<div style="background-color: seashell; ...">A</div>
<div style="background-color: crimson; ...">B</div>
A
B

Now we can force one to overlap over the other by setting a negative top margin on B:

<div style="background-color: seashell; ...">
  A
</div>
<div style="background-color: crimson; margin-top: -1rem; ...">
  B
</div>
A
B

But what determines which div should be on top in this case?

The order in which they appear in the HTML source code determines how they overlap.

Specifically, elements defined further along or later in the HTML source code will sit above ones that where defined earlier.

In this case B above A.

But more detail later on…

Setting the z-index to some value

What is we wanted div A to be on top of B in our example above?

If you want to be explicit about the order in which elements ‘stack’ on the page, you need to set z-index: to a relevant value

The z-index: can be set to a positive or negative integer (or auto and inherit).

But what number should you set it to?

And why does it sometimes not seem to work?

The z-index only applies to elements in certain situations

Lets look at our previous example of divs A and B.

If we set the z-index on A to some number higher than on B we might expect A to now sit on top:

<div style="z-index: 10; ...">
  A
</div>
<div style="z-index: 0; margin-top: -1rem; ...">
  B
</div>
A
B

But that didn’t change anything!

Let’s start to explore why…

In order for the z-index property to have an effect on an element, the element must either:

  • have it’s position: attribute set to either relative, fixed, absolute, or sticky, but not static.
  • or use the flex or grid display modes.

Why? More on that later.

The catch is that static is the default position: mode. So in our example above A is position: static

Thus if you only specify a z-index: without setting the position: or display mode, it will have no effect.

That’s the first reason why your z-index: might not be working!

Let’s try our example again but set position: relative on A:

<div style="z-index: 10; position: relative; ...">
  A
</div>
<div style="z-index: 0; margin-top: -1rem; ...">
  B
</div>
A
B

Success! A is now on top of B.

And the z-index is relative to the ‘stacking context’

But there is another thing…

Many people don’t realize that the z-index: value is relative to a specific context (grouping of HTML elements).

So to understand how the elements will stack requires an awareness of the so called stacking context of the element.

The Stacking Context

A stacking context is a group of elements that move together as a group along the z-axis.

The z-index: value of any elements in the stacking context group, is only related to the other elements in that same group.

The parent element of the group then determines the stacking order of the group as a whole.

Let’s repeat that.

Setting an element’s z-index: higher than any of its siblings elements (elements in the same ‘stacking context’) will stack it on top of those siblings.

But no matter the z-index: value, the element will not stack relative to any elements outside of its group.

The parent element’s z-index: will determine where the group as a whole sits, relative to other elements around it (ie relative to the parent element’s ‘stacking context’).

Creating a ‘stacking context’

Consider back to our previous example with divs A and B. First B was pushed over A with a negative margin:

A
B

We then set the position: to relative for A, and that caused it to be on top of B.

A
B

Ok now let’s try set the position: of B to relative instead:

<div style="z-index: 10;...">
  A
</div>
<div style="z-index: 0; position: relative; margin-top: -1rem; ...">
  B
</div>
A
B

Hm… B is on top of A again. The z-index of A has not caused it to stack on top.

Look carefully however and you will notice that this time the B element actually “hides” the text of the A element.

It is no longer overlapping the A element, but rather it is covering it.

Its subtle, compare it to the first example where B was pushed over A with a negative margin:

A
B

So what does this mean?

It means that the position: relative attribute on B has caused it to create a new ‘stacking context’ for itself. This makes the renderer consider B as “stackable”. The elements are no longer just overlapping. B is now “stacked” on top of A.

A on the other hand is not stackable because it is not in a ‘stacking context’. Thus the z-index of A is not relevant to the stacking order of B!

Now let’s make A join B in the same ‘stacking context’.

If we set the position: of A to relative also, so both are now positioned relatively to their parent, then they end up being siblings in the same ‘stacking context’ and the z-index of A is now relevant to the stacking order of B.

<div style="z-index: 10; position: relative; ...">
  A
</div>
<div style="z-index: 0; position: relative; margin-top: -1rem; ...">
  B
</div>
A
B

And you guessed it, B is now on top of A.

Other ways to create a ‘stacking context’

Certain CSS properties force an element to have a new ‘stacking context’.

position: as mentioned earlier, but also opacity: and transform: & others.

You can also explicitly create a grouping using the isolation: isolate property, which is useful if you don’t want or need to use any of the other properties!

For example, instead of using position: relative on both elements, we can use isolation: isolate on both instead:

<div style="z-index: 10; isolation: isolate; ...">
  A
</div>
<div style="z-index: 0; isolation: isolate; ...">
  B
</div>
A
B

A “stacking context” is required for z-index to work

So thinking back to earlier in this article, we said that the z-index: value of an element is only applied if say the element has position: relative or other attributes set.

Now it makes sense why. By setting the position: attribute to relative we are creating a new ‘stacking context’ for the element.

Initially, when all we did was set z-index: values, the elements were not in a ‘stacking context’ and so the z-index: values were ignored.

The default ‘stacking order’

By default, elements in a stacking context that have no z-indexes set, are stacked in the order they appear in the HTML document.

This is known as the auto z-index mode.

Since it is the default you normally don’t specify it, but if you did, (for example to override a value set by another CSS declaration) you would do z-index: auto;.

The z-index at work: an example

Let’s look at a more complex example:

<div style="z-index: 1; isolation: isolate; ...">
  <div style="z-index: 10; position: relative; ...">
    A
  </div>
  <div style="z-index: 1000; ...">
    B
  </div>
</div>
<div style="z-index: 3; isolation: isolate; ...">
  C
</div>

What do you think the stacking order will be?

Answer: C on top, A in the middle, B at the bottom.

Why: Because C is in a stacking context with the parent element of A and B. They are siblings and the one with the highest z-index: sits on top (ie C is over the group of A and B).

So the 2 child elements A and B stack according to their parent when considered to div C.

Now inside that first div however, even though B has a much larger z-index: value that A, A actually creates a new stacking context (as it is position: relative) but B does not (in fact is it position: static by default). So B is not in a stacking context and the z-index: of B is not relevant!

A
B
C

Learning more

For an article specifically about z-index go to Z-index and stacking contexts on web.dev and for one on stacking contexts go to Stacking Contexts on Josh Comeau’s blog. Both of these articles are excellent!

I highly recommended Josh Comeau if you want to learn about CSS. His articles are understandable, visual, well researched and dive deep into the topic.

Also checkout web.dev as another excellent source.

Found that useful? Subscribe for more

Keep up to date with all my content by subscribing to my very low traffic and zero spam substack.

You can also find me on Mastodon and Bluesky

Lastly, just for fun, my original notes on z-index!

CSS z-index: what is it and how does it work

CSS z-index:: my notes on what it is and how it works

CSS 'z-index:auto' default stacking order

z-index: auto the default stacking order

CSS 'z-index' with 'position:relative'

How z-index: applies with the elements position: attribute set

CSS z-index is relative to the 'stacking context'

Imagine the second div at the root has a different stacking context to the one above it. The z-index: is always relative to the ‘stacking context’ of the element!