CSS stacking contexts and z-index made easy

5 minutes read

If z-index: 99999; is something you write often when you work with CSS, this story is for you. We will see how to handle the stacking context and the z-index property in your CSS.

Stacking context

This term indicates a three-dimensional conceptualization of the HTML elements that are stacked across the imaginary z-axis, based on some attributes these elements can stack on top of each or inherit properties from their parents.

A practical and common example of this is the CSS opacity property. When you apply this property to an element, every child element, at any level, will be rendered with the same opacity level. This property will cause the element to create a stacking context.

Let's see what CSS properties and HTML elements trig a stacking context:

While most of the above conditions can be rare, let's take the most common one: an absolute-positioned element inside a container. When we want to set the coordinates of an absolute element and make them relative to a parent boundary we add position: relative on that parent element, creating in fact a stacking-context (see the second point above)

You can consider the stacking-context as a virtual cardboard box seen from the top, that stacks elements inside it, but without any solid boundaries.

z-index and voodoo magic

The z-index property let you define the position of the element across the Z-axis, inside a stacking context — from here referenced as "box". If you take two real cardboard boxes, you know that you can put one on top of each other, and you can put many elements inside each of them. The elements that are inside the upper box will be positioned (or rendered speaking of CSS) above the elements that are inside the underlying box. The only difference is that in CSS, these boxes haven't "solid" boundaries, and elements can overflow outside them.

Let's take this markup structure:

<div class="A">
<div class="One"></div>
<div class="Two"></div>
</div>
<div class="B">
<div class="Three"></div>
</div>

Here the visual representation of these two boxes, with some CSS applied:

Side view of a visual representation of the CSS stacking-context

Are you still there? Now, follow me.

In the above image, .A and .B are our two boxes (stacking-contexts) and they are siblings inside the same HTML parent. The .B element is projected and rendered above .A cause its z-index property, in this case our <HTML> element is the container and the main context holder as stated in the first point of the above list.

Now, you can notice that .B is not the only element rendered above .A, but also his content, more precisely the .Three element. You can imagine .One and .Two as books inside our cardboard boxes. .Two is above .One but they still are both inside the box .A, and so rendered under the box .B.

Here is the visual representation of the same structure, but from the top view, or in other words, from the user point of view.

Side view of a visual representation of the CSS stacking-context

For this reason, putting an over 9000 z-index on the element .Two doesn't make it rendered above .Three, because it's inside a stacking context that has a lower priority compared to sibling elments and contexts.

Handling indexes

The stacking-context system is pretty easy once you get it, the hard part is managing your CSS code and keep it organized. My approach is to define one big document scope in which define a bunch of layers, and multiple smaller scopes when required inside each components.

Document scope

I define 3 z-index levels to use across the whole project, like for dropdowns, fixed headers, modals, and contextual popups.

Supposing to write a correct HTML structure, every elements will be start from level 0 (initial) by default, popups and contextual dialogs will use the level 1, fixed headers will use the level 2 while modal dialogs will use the level 3. With just 3 level of z-index you can probably handle 99.9% of the use cases. The key is to assign the appropriate level to UI elements based on their role and usage inside the whole UI.

Smaller scopes

Each UI component are placed on the level 0 but they can define as many levels as required inside them. So if needed, each component can create a "private" stacking context in which handle its elements and z-indexes, al starting from the level 0 of the document scope.