Complete guide to z-index in CSS

Kirill Shelipov
Nerd For Tech
Published in
5 min readSep 8, 2021

In modern web development, it is very common to use z-index. But many people don’t fully understand how it works. Because of this, developers occasionally encounter problems, which are then difficult for them to solve. Let’s dive deeper into this topic.

What is z-index in general? This is a CSS property that specifies the order in which the elements will be displayed when they overlap. Right? No, not really. In fact, the overlapping order depends on several parameters. Take a look at the following code example:

Code example

As you can see, the parent has position: relative. The child element has absolute positioning and a z-index, which is -1. But for some reason, the child element is still on top. Looking at the code again, you might notice a strange filter: blur(0px). I added this line for a reason :)

Let’s see what happens if we remove this line:

But you may ask: What does the filter have to do with the z-index at all? The answer is the context.

The point is that the z-index works only within its context. What is a stacking context? It is an area along the spatial z-axis along which elements are lined up. And z-index will work within one such area. This means that it will not work in another!

Here is the definition from MDN:

The stacking context is a three-dimensional conceptualization of HTML elements along an imaginary z-axis relative to the user, who is assumed to be facing the viewport or the webpage. HTML elements occupy this space in priority order based on element attributes.

Okay, now we understand that z-index works only in contexts. But how are they created?
One option is intuitively clear to all of us because everyone has used it:

Positioning other than static and z-index other than auto

The parent created a new context, which causes the child element to now use z-index: -1 in a different context. Therefore, the elements are simply displayed according to their order in the DOM. Before that, the parent did not create a new context and both elements used the context of the HTML tag.

But there are still many situations that create a new context. Let’s break down each one so that you can quickly find the causes of problems in your layout in the future.

In all situations, the starting position will be the same: I will try to “break” the z-index logic of the child element by creating a new context on the parent. So, in each example z-index: -1 will not work as we expect by default.

The parent of an element is a flex / grid container and the element has a z-index different from auto

As you can see, because the parent element has its own z-index and is part of a flex container, it creates a new context. Try to remove display: flex from the topmost element, and you will see that no new context will be created, even though the z-index: 1 of the parent element. Interesting… What else?

Element with an opacity value less than 1

The most counterintuitive case for me. Imagine, transparency affects the order in which elements are displayed along the z-axis! In fact, changing transparency below 1 just creates a new context. I once couldn’t understand why a child element would not fall below the parent in any way. I set the z-index to -1000000, but it didn’t help :) It was the transparency of the parent.

mix-blend-mode other than normal

Do you want to use blending? Be prepared for it to break your z-indexes. But let me upset yours even more…

An element with any functional properties, such as transform, filter, perspective, clip-path, mask, mask-image, mask-border other than none

Now, look at the first example from the article again :) Now do you see what this is about? filter: blur(0px) creates a new context. So if you want to use new and cool features of CSS, keep this feature in mind. Sometimes you will have to use box-shadow instead of filter: drop-shadow(), for example.

Other cases

The other cases are rarer, so we will mention them in passing.
From the MDN spesification:

Element with a isolation value isolate.

Element with a -webkit-overflow-scrolling value touch.

Element with a will-change value specifying any property that would create a stacking context on non-initial value (see this post).

Element with a contain value of layout, or paint, or a composite value that includes either of them (i.e. contain: strict, contain: content).

But that’s not all!

Did you know that if you set (for example randomly) the background color on both HTML and BODY at the same time, ALL elements with z-index less than 1 will “break” and “fail” behind the background! In fact, this does not happen to all elements whose z-index is less than 1. It happens to elements that are within HTML context (first rule of creating context from MDN specification). By changing the background of both BODY and HTML, we somehow “break” that context. Try assigning a background just to HTML or BODY — it will work!
The standard case is that you’ve created a React app using create-react-app. In the created styles file you decided to add a background color. But instead of the BODY block, you added it to the HTML, BODY block. And then you forgot about it. Trust me, if you try to figure out why your z-index: -1 doesn’t work in the future, you’ll kill hours of time. But now you know what the problem could be ;)

Conclusion

Z-index is a very simple property at first sight. But when problems related to it occur, not everyone understands why they happen. Now we know that it is most often about the context in which the z-index is used.

Add me on Linkedin: https://www.linkedin.com/in/kirill-shelipov-b8ab461a2/

--

--