OZMap! The ultimate z-index solution
How ordered maps solve the final issue with z-indexes: 3rd party ones
Countless bytes have already been written about the issues with maintaining z-indexes and avoiding the classic problem of “battling 9’s” (i.e. “I need A to stack on top of B; let me just add another 9 to the z-index”). As such, I won’t go into too much detail about that particular aspect of the challenge; instead I will focus on a particular pitfall that I’ve never seen addressed before.
Firstly, though, I will give a brief background so that the necessary details are in place. Z-indexes are how developers can specify which element should be on “top” when multiple elements overlap. A basic example is shown:
z-index: 1; z-index: 2
z-index: 2 z-index: 1;
This works fairly well in many simple cases. The issue becomes one of maintainability - as your site grows and components elements get reused in multiple contexts, it can be hard to keep track of all the different z-indexes you may rely on. For example, at my last check NBC News had 21 unique z-indexes defined, ranging from -1
to 20000000
.
Reconceiving z-indexes
The problem partially stems from a misguided view of what a z-index actually is. Technically, z-indexes are cardinal numbers, where the value of the number matters intrinsically. As an analogy, money can be thought of as cardinal, since $100 is better than $1.
Conversely, an ordinal number is one that delineates an order, e.g. First, Second, Fifth, and the like. Think of a race, where the actual time each person runs is almost immaterial (save for things like world records); what matters is who comes First, who comes after, and so on.
Z-indexes are best visualized as ordinals. To use our above example
z-index: 1; z-index: 2
z-index: 1 z-index: 999;
A z-index of 999
is no better than 2
– the only thing that matters is a sufficiently large z-index for the element you wish to appear on top.
Using Sass lists
Sass lists are a great way to use this new way of thinking in practice. Instead of having to keep track of a slew of different numbers, you can just make a list with the elements in the order they should stack, then reference the desired element with the index
function:
$elementList: (main, header, modal);
main {
z-index: index($elementList, main);
}
header {
z-index: index($elementList, header);
}
.modal {
z-index: index($elementList, modal);
}
which would compile to
main {
z-index: 1;
}
header {
z-index: 2;
}
.modal {
z-index: 3;
}
To make this method especially cool, since the z-indexes are generated at compile-time we no longer have any issues if we need to slip a new element into the mix. Adding something like a notification
element into the list before the header
element would automatically bump the generated z-index for header
to 3.
In my opinion, this is the absolute best way to handle z-indexes – with one caveat. What do you do if there’s a 3rd party element on your page that defines its own z-index, and it doesn’t fall nicely into your list of elements?
An alternative approach that I’ve seen proposed is to create a map of your z-indexes, so they are all organized in the same location, something like
$zMap: (
main: 1,
header: 2,
modal: 3,
)
This solves the issue of arbitrary z-indexes posed by 3rd party elements, but it removes the benefit of ordered & automatically incrementing elements. Can we get the benefits of each of these approaches?
Ordered z-index maps (OZMap) to the rescue
With a little bit of ingenuity I believe we can eat our cake & have it, too. We do this by creating a new kind of map with the following 2 rules:
- The elements of the map may contain
null
(to indicate that the z-index value should be set automatically) - The elements of the map may contain a hardcoded number, to be used as the z-index for that element.
- Note that the value must be greater than any preceding element, to maintain the order
An example of what such a map and a function to use it might look like:
$globalZIndexes: (
a: null,
b: null,
c: 2000,
d: null,
);
@function getZIndex($listKey) {
@if map-has-key($globalZIndexes, $key: $listKey) {
$zAccumulator: 0;
@each $key, $val in $globalZIndexes {
@if $val == null {
$zAccumulator: $zAccumulator + 1;
$val: $zAccumulator;
}
@else {
@if $val <= $zAccumulator {
//* If the z-index is not greater than the elements preceding it,
//* the whole element-order paradigm is invalidated
@error "z-index for #{$key} must be greater than the preceding value!";
}
$zAccumulator: $val;
}
@if $key == $listKey {
@return $zAccumulator;
}
}
}
@else {
@error "#{$listKey} doesn't exist in the $globalZIndexes map";
}
}
.a {
z-index: getZIndex(a);
}
.b {
z-index: getZIndex(b);
}
.c {
z-index: getZIndex(c);
}
.d {
z-index: getZIndex(d);
}
which would compile to
.a {
z-index: 1;
}
.b {
z-index: 2;
}
.c {
z-index: 2000;
}
.d {
z-index: 2001;
}
Thus, we have achieved both convenience with auto-incrementing z-indexes AND flexibility for 3rd-party defined z-indexes.