The z-index CSS property specifies the z-order of a positioned element and its descendants. When elements overlap, z-order determines which one covers the other. An element with a larger z-index generally covers an element with a smaller one. — z-index by MDN
Working with z-index values in large projects can be really difficult. On a growing project, you can observe the z-index values increasing. At some point it is not uncommon to see values like 999. If you want to add a new component with a custom z-index it is hard to test all affected components and its impact. Worst case scenario, you have to go through multiple files and compare z-indices, then build and test again and so on. This can be really frustrating because you end up adding or removing a digit to an already unnecessary high value. This way of handling z-index values leads to many bugs such as components that falsely overlay other components. We encountered the same problem in one of our projects and discovered that a lot of bugs were caused by conflicts between z-index values.
Part of the problem is that z-index is mostly misunderstood because the concept of the stacking context isn’t widespread, although they are strongly linked. So let’s start with this:
At the beginning of our journey we discovered that we could reduce the use of z-index by ~50%, caused by z-indices for hovering elements. In some cases it is enough to simply set the z-index to 1 and leave the underlying element without any value.
Another common issue is the use of „position: absolute“ in combination with a very high z-index. Browsers render absolute positioned elements with a very high stack context, which allows them to stay most of the time at the highest position. It is redundant to add yet another gigantic z-index.
This is one of the minor misunderstandings we discovered but probably the biggest issue:
Children inherit the parents‘ z-index relative to neighboring elements of the parent. Confusing, right? We felt the same way but it is important to understand this part – it will pay out in the end.
Inspired by: The stacking context (MDN)
The following bullet points describe the stacking context of the image above:
If you want to dive deeper into stacking context go check out the enlightening MDN-Article. Understanding the stacking context is key to successfully handle your z-index values. With that in mind we tried to find a solution where we can map the stacking context into our code.
With that knowledge we worked out some requirements:
Sass provides the right toolbox to master the z-index chaos. On our search for an existing solution, we found this article among many others, which fulfilled our requirements and forms the basis for our solution. A meaningful and sorted list with key value pairs is the best approach to start organizing z-index values coupled with a getter function (in our case simply called z). Like so:
$z-layers: (
"dropdown": 150,
"header": 75,
"content": 60,
"footer": 50
);
Above we see a very simple list of a few components. The elements in the list allow us to directly create a context by assigning a name to the z-index instead of a meaningless number somewhere deep inside the code. The elements in the list are sorted, so the stacking context is immediately clear.
Let’s have a closer look how to assign one of our managed z-index values:
.dropdown {
...
z-index: z('dropdown');
...
}
z is a wrapper function that returns the desired value. The function simply iterates through the list and searches for the given key (if you want to see the whole implementation, the example is attached at the end of this post).
Usually, one z-index value is not sufficient for a large component. In the example above we spoke about „parent stacking context“. For example, you don’t want your nested elements inside the header to clash with the footer.
$z-layers: (
"header": (
"base": 150,
"account-dropdown": 45,
"searchbar": 30,
),
"footer": 50
);
To represent the stacking order we need to nest $z-layers. The base value is assigned to the parent container, in our case to the header. The children (account-dropdown and searchbar) are in the context of the header container thus have a higher stacking order than the footer. So you don’t have to worry about exceeding the z-index of the footer for every element inside the header. This also allows you to reduce the z-index values since they globally share the same stacking context as the header.
Another benefit of this approach is that, while reading the code it is much more clear how the nested elements are stacked. The usage is pretty much straight forward:
.searchbar {
...
z-index: z("header", "searchbar");
...
}
With this strategy we eliminated our z-index related issues almost entirely. Before managing our z-indices, adjusting the value of one element could unexpectedly break the layout for other components. It is convenient to have one single point of z-index assignments. All the mentioned benefits can easily be achieved but don’t forget that you need discipline when maintaining the z-index list.
/* z map definition */
$z-layers: (
'header': (
'base': 30,
'account-dropdown': 10
),
'blue': 20,
'red': 10,
'green': 5,
'default': 1,
'zero': 0
);
/* helper functions */
@function nested-keys($map, $keys...) {
@each $key in $keys {
@if not map-has-key($map, $key) {
@return false;
}
$map: map-get($map, $key);
}
@return true;
}
@function deep($map, $keys...) {
@each $key in $keys {
$map: map-get($map, $key);
}
@return $map;
}
@function z($layers...) {
@if not nested-keys($z-layers, $layers...) {
@warn "No z-index entry found";
}
@return deep($z-layers, $layers...);
}
/* usage: */
.red {
background: red;
top: 0;
left: 0;
z-index: z('red');
}
.account-dropdown {
z-index: z('header', 'account-dropdown');
}