TL;DR
- Z-Indexes can get messy fast.
- It's common to pick z-indexes by guessing!
- There are some ways to improve this, but they only work to a point.
- We can automate our z-index values generation and solve most of those issues.
The Problem with Keeping Track of Z-Indexes
Z-index is a relative CSS property. It has no unit of measurement, other than the other z-indexes in the project. Those other values are usually spread all over the project, which leads to interesting phenomenons.
How Z-Indexes can get messy?
Let's say you have a modal with the value of 999999.
We add a date picker that should be behind it but above everything else, and write 99999 so you'll have some buffer.
A year later, a colleague has to add an error popup. It should be over everything, including the modal. He sets its z-index to 9999999999. He didn't know (or forgot) about an old ad component a third colleague, long gone, added with a z-index value of 999999999999.
Now, in one of the many pages of your project, an ad will pop up that hides the error.
Another similar bug could make your date picker unusable, and another - hide your modal's "buy" button.
Using Orders of 10 to Improve Workflow
One common way to guess less is to work with powers of 10. You add a lot of zeros to something that should be on top, and less zeros to things under it:
/* This is the most toppest thing ever! */
.modal {
z-index: 10000000;
}
/* Oh wait */
.error {
z-index: 10000000000;
}
Another common method is to use any number of 9s:
.modal {
z-index: 99999999;
}
.error {
z-index: 9999999999;
}
That one is extremely popular.
As your project gets bigger, and more people are working on it, this tends to become a guessing game, where a developer writes a very big number and hopes for the best:
.old-thing {
/* I want my thing to always be on top of other things */
...
/* This should be enough to always be on top. */
z-index: 1000000;
}
.new-thing {
/**
This should hide everything, even old-thing.
It should also hide things we add in the future.
**/
...
/* I'll write a large number to make that happen */
z-index: 10000000000;
}
/**
Wow, I've had to guess a lot of zeros here.
I can't change other z-indexes because I don't know what
I'll break, but I have to guarantee it'll always be on top.
**/
.newer-thing {
z-index: 1000000000;
}
"Whoops, new-thing still hides this. I'll fix that:"
.newer-thing {
z-index: 1000000000000000;
}
We end up with different magnitudes of numbers that don't make sense except them being guesswork.
It's also harder to understand the actual order of things as you use bigger and bigger number. When comparing 1000000000000000
and 10000000000000000
, you'll have to count zeros to understand who's hiding who.
Using multiples of 10, 100 or 1,000
Another common workflow. It looks like this:
.menu {
z-index: 100;
}
.sales-notice {
z-index: 200;
}
.error {
z-index: 300;
}
This is an improvement over the orders of magnitude method; It's easier to tell which component will be on top of which, as we now have a single order of magnitudes and a uniform difference between the different z-indexes.
However, it still suffers from some of the problems of the previous method; When you have to add a new layer between two existing one, you'd use the gap between the two and pick their average:
/* This hides the menu and hides behind sales-notice */
.menu {
z-index: 100;
}
.tooltip {
z-index: 150; /* Hmm */
}
.sales-notice {
z-index: 200;
}
.error {
z-index: 300;
}
And just like that we lose the uniform gap advantage. When digging into the code (or a new section of it) for the first time, we can't intuitively know if the smaller difference between the menu and the tooltip means something.
Also, as the project grows, developers will still start to guess numbers. They won't be gargantuan guesses, but it'll make the meaning of each number hard to understand.
Using an Object/Map/List to Manage Your Indexes.
Here you concentrate all of your z-indexes in the same place:
const zIndexes = {
menu: 100,
error: 200,
}
// Use those value as CSS variables. We'll get to this soon.
injectZIndexes(app);
.menu {
z-index: var(--z-index-menu);
}
.error {
z-index: var(--z-index-error);
}
This is a major improvement over the previous methods; Because you manage all of your z-index values in a single place, it's easy to see their order. Just as importantly, you can understand their purpose, now that they're named.
However, we still pick middle numbers when adding middle layers, so you can't easily tell the meaning of the numeric differences once your project grows:
const zIndexes = {
menu: 100,
tooltip: 125,
modal: 150,
error: 200,
loadingScreen: 300
}
You can keep a constant, meaningful difference, but it'll require you to rewrite all of the values greater than the new addition.
To Summerize:
All of the methods above requires you pick numbers in a meaningless way, many times with guesses. They all become messy as your codebase grows.
A Better Way
Optimally, we'll have an object with constant differences between each layer:
({
'z-index-menu': 100,
'z-index-modal': 200,
'z-index-error': 300,
})
The common thing to all of those methods, and source to most of those problems, and the reason we can't have nice things, is that you have to manage the values yourself.
Since you don't really care about the exact numbers, only about their relative values, we can do much better - we can let a tiny bit of code to take care of that for us:
const makeZIndexes = (layers) =>
layers.reduce((agg, layerName, index) => {
const valueName = `z-index-${layerName}`;
agg[valueName] = index * 100;
return agg;
}, {});
);
When we use it, we get an object with all the variables you need for z-indexes, nicely named and automatically numbered:
const zIndexes = makeZIndexes(
['menu', 'modal', 'error']
);
// Which will give us:
({
'z-index-menu': 100,
'z-index-modal': 200,
'z-index-error': 300,
})
To add another layer, just add its name in the array:
const zIndexes = makeZIndexes(
['menu', 'clippy', 'modal', 'error']
);
// The result:
({
'z-index-menu': 100,
'z-index-clippy': 200,
'z-index-modal': 300,
'z-index-error': 400,
})
What Did We Do Here?
- We created a small array of names. By creating named variables, we can now know where each of them is used.
- The numbers are automatically generated! We don't have to guess and hope anymore. We outsourced this concern to a few lines of code.
- The names array is the only code we have to change to manage our z-Indexes. This is a very simple interface, that is managed from a single place.
- The top layers' values have changed! Since the z-index number only matters because of its relation to other z-indexes, and since all of those indexes are managed here in a way that keeps their order, we don't care!
- Equal difference between every two adjacent layers. This is easier to read, as you don't have to figure out the reason for different differences.
How To Use Those Z-Indexes?
That depends on your style framework. This method is easy to implement in any method, CSS preprocessor or framework you'd like:
Vanilla (JS-generated CSS variables) (example):
const Z_INDEX_LAYERS = ['menu', 'clippy', 'modal', 'error'];
const zIndexes = makeZIndexes(Z_INDEX_LAYERS);
// Format as CSS variables and inject to a top HTML element
const styleString = Object.entries(zIndexes)
.map(([name, value]) => `--${name}: ${value}; `)
.join('')
document.querySelector('.app')
.setAttribute("style", styleString);
.menu {
...
z-index: var(--z-index-menu);
}
SASS (see it working here):
$z-layers-names: ("menu", "sales-notice", "error");
$z-layers: ();
$i: 0;
@each $layer-name in $z-layers-names {
$i: $i + 1;
$css-var-name: "z-index-" + $layer-name;
$z-layers: map-merge(
$z-layers,
($css-var-name: $i),
);
}
.menu {
...
z-index: map-get($z-layers, "menu");
}
The vanilla solution, by the way, is universal. Your CSS preprocessor, or CSS-in-JS framework, should support this widespread feature.
Simply run the JS part, inject to a DOM element and use CSS variables wherever you'd like.
Related Z-Index Grouping (and Why We Still Use Multiples of 100)
We can use sequential numbers, but using multiples of a larger number gives us a convenient way to manage related z-indexes.
For example, a modal close button is related to the modal, and will always move with it - even when changing the order of layers. We can easily express this in CSS:
.modal-close-button {
...
z-index: calc(var(--z-index-error) + 1);
}
I implemented this as a universally usable npm library, by implementing it as an Inventar plugin.
Inventar is a tiny, powerful, framework-agnostic theme & style logic manager. Among other things, it can convert your configuration to a function that injects CSS variables into an element's style.
This is how it looks like:
import makeInventar from 'inventar';
import zIndex from 'inventar-z-index';
const Z_INDEX_LAYERS = ['menu', 'clippy', 'modal', 'error'];
const { inject } = makeInventar({
...
zIndex: {
value: 100,
transformers: [zIndex(Z_INDEX_LAYERS)],
},
});
Questions? Complements? Complaints? Chocolate surplus? Let's talk about it in the comments or in my LinkedIn.
Thanks to Yonatan Kra for his helpful, thorough review.
Top comments (10)
IMHO if you end using a lot of z-indexes that way then there is something else to fix. We generally don't need so much z-index.
I agree. In particular, if you lay on a lower stacking context no z-index class would help.
Just do this
Nobody will beat you in a z-index war 😂🤓
There is actually a maximum value for z-indexes. it's 2147483647 in most browsers.
stackoverflow.com/questions/491052...
I know, there was a famous YT vid on the subject a few years back, but that won't stop me trying, besides 9 × 10³ is nowhere near the theoretical limit so we won't get arrested 🚨
Why not both? :P
codesandbox.io/s/a-better-way-to-m...
Would you rather have a pet 🐴 or 🦓? I prefer simplicity, it's much harder to clean a zebra than a horse, sincerely a mad developer
The first few examples remind me of programming in BASIC, where one could run into similar issues with its line numbers. 😅
On a more serious note, wouldn’t different stacking contexts make z-indexes even more manageable?
Yes, stacking context is the key.
I usually find problems when trying to set something above the rest, but "the rest" is laying on another, higher stacking context.
Giving whatever z-index to my object wouldn't solve: I need to change an ancestor.
I understand some of the things pointed out in the comments, about stacking context being the most important thing to worry about. I would consider this to be a solution for managing the top-most stacking context: modals, errors and messaging should go over the main content, and if multiple things happen at once, you want to make sure the right things go on top. I find this a pretty interesting take on that.