Z le maudit

Better z-index management with Sass

Who wants a better solution than throwing a random number in a z-index and hope for the best?

4 min readApr 25, 2020

--

The situation

In a complex application, developers will always compete for the highest number to put an element on top of the rest, without easily knowing what layers are already in place.

One cannot simply just write:

.my-modal {
z-index: calc(on-top-of-the-page);
}
.tooltip-on-my-modal {
z-index: calc(on-top-of-my-modal);
}

So most of the time you end up in a situation like this:

But these z-index numbers are:

  • Totally arbitrary
  • Nearly impossible to maintain
  • More importantly, might not even have the intended behavior

Why is that? Because z-index is not an absolute value, it depends on the stacking context.

On the other hand, the problem in complex applications is the maintainability of all leveled elements. If you’re using z-index 1, 2, 3, 4 and so on, the day you want to add something between 3 and 4, you have to change the z-indexes of all the layers after 3…

To mitigate this problem, some developers are establishing conventions like:

  • Base: z-index: 0
  • Hover: z-index: 100 and beyond
  • Modal: z-index: 200 and beyond
  • Etc.

But that doesn’t really solve all the aforementioned problems, it just allows for more gaps between levels… for a time.

What should be the z-index of the next hover element? z-index: 101 or z-index: 150 or…

Proposed approach

We can leverage the capabilities of Sass maps to handle the layering system.

The principle of this approach is to surface the layers used in an application by expressively naming them in a Sass map and retrieving their z-index by calculating their index in the map.

What’s the best part of this technique? It can hold nested elements and mimic your application’s DOM tree structure in a simpler representation!

Example of a basic map in Sass:

$layers: (
main: (
base: (),
card: (
base,
raised,
tooltip
),
raised: (),
tooltip: ()
),
menu: (
base,
tooltip
),
navigation: (
base,
tooltip
),
modal: (
base,
tooltip
)
);

Note that all elements in the map need to be a real element of your DOM.

Now, to retrieve the z-index values you need something like this:

@function utility--z-number($keys) {
$map: $layers;
$found-index: null;
@each $key in $keys {
@if (type-of($map) == "map") {
$found-index: index(map-keys($map), $key);
$map: map-get($map, $key);
}
@else {
$found-index: index($map, $key);
}
@if ($found-index == null) {
@error "`#{$key}` is not part of the elevation map: `#{$layers}`";
}
}
@return $found-index;
}
@mixin layout--layer($keys...) {
$first-el: nth($keys, 1);
$valid-positions: relative, absolute, fixed, sticky;
@if (index($valid-positions, $first-el)) {
position: $first-el;
z-index: utility--z-number(utility--list-remove($keys,1));
} @else {
position: relative;
z-index: utility--z-number($keys);
}
}

A couple of remarks on this bit of code:

  • The function is returning an integer (or an error if it cannot find the value passed)
  • The mixin has the option to pass a position as a first argument if you don’t want it to be relative. This is because, in order to make z-index work, your DOM element needs to be positioned.
  • We also use a utility function to remove the first argument before passing the value to the function, but for the sake of brevity, I won’t detail it here.

How to implement this approach

In a design system, the key concept to grasp is that every application that uses your system will have a different layering map. You can provide a default map as an example, but it should be overridden in every application that wants to implement this technique.

As you might have guessed, it’s much simpler to use this approach from the start than to implement it afterward.
Refactoring all the layered elements would be tedious and potentially very challenging since, to make it work, you might need to touch a lot of files to remove all the arbitrary z-indexes (and probably with much higher numbers). It’s an all-or-nothing scenario, you can’t progressively add it.

Afterward, you might also want to implement a linter rule in your code base to forbid future hard-coded values (see this StyleLint plugin for example).

In conclusion

If we take back the example at the beginning of this article, you can now write in your source code expressions like:

.my-modal {
@include layout--layer(modal);
}
.tooltip-on-my-modal {
@include layout--layer(absolute, modal, tooltip);
}

Which will be compiled as:

.main {
position: relative;
z-index: 4;
}
.nested {
position: absolute;
z-index: 2;
}

I hope you find this technique useful, don’t hesitate to reach out if you have any comments or questions!

--

--

Challenger of habituation on a mission to improve humanity, one idea at a time. Design system lead & consultant. Host of @DSSocialClub. Mentor on ADPList.