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:

Default

z-index: 1; z-index: 2
Changing overlap with z-index

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

Default

z-index: 1; z-index: 2
Very large z-index

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:

  1. The elements of the map may contain null (to indicate that the z-index value should be set automatically)
  2. 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.