DEV Community

Miki Stanger
Miki Stanger

Posted on

A Better Way to Manage Z-Indexes

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;
}
Enter fullscreen mode Exit fullscreen mode

Another common method is to use any number of 9s:

.modal {
  z-index: 99999999;
}

.error {
  z-index: 9999999999;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
.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;
}
Enter fullscreen mode Exit fullscreen mode
/**
  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;
}
Enter fullscreen mode Exit fullscreen mode

"Whoops, new-thing still hides this. I'll fix that:"

.newer-thing {
  z-index: 1000000000000000;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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); 
Enter fullscreen mode Exit fullscreen mode
.menu {
  z-index: var(--z-index-menu);
}
.error {
  z-index: var(--z-index-error);
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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,
})
Enter fullscreen mode Exit fullscreen mode

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;
  }, {});
);
Enter fullscreen mode Exit fullscreen mode

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,
})
Enter fullscreen mode Exit fullscreen mode

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,
})
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
.menu {
  ...
  z-index: var(--z-index-menu);
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)],
  },
});
Enter fullscreen mode Exit fullscreen mode

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.

Oldest comments (10)

Collapse
 
chuniversiteit profile image
Chun Fei Lung

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?

Collapse
 
massic80 profile image
Massimiliano Caniparoli

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.

Collapse
 
adam_cyclones profile image
Adam Crockett 🌀

Just do this

div {
    z-index: 9e1;
    z-index: 9e2;
    z-index: 9e3;
}
Enter fullscreen mode Exit fullscreen mode

Nobody will beat you in a z-index war 😂🤓

Collapse
 
larsejaas profile image
Lars Ejaas

There is actually a maximum value for z-indexes. it's 2147483647 in most browsers.

stackoverflow.com/questions/491052...

Collapse
 
adam_cyclones profile image
Adam Crockett 🌀

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 🚨

Collapse
 
mimafogeus2 profile image
Miki Stanger
Collapse
 
adam_cyclones profile image
Adam Crockett 🌀

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

Collapse
 
afif profile image
Temani Afif

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.

Collapse
 
massic80 profile image
Massimiliano Caniparoli

I agree. In particular, if you lay on a lower stacking context no z-index class would help.

Collapse
 
justusromijn profile image
Justus Romijn

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.