The z-index Arms Race

Aaron Dilley
3 min readJan 4, 2017

--

Of all CSS properties, z-index may be the most frustrating to deal with on a site featuring multiple, separately maintained stylesheets. Fixed UI and intentionally disruptive page components demand the foreground. z-index is the quickest way to lay claim to what the user will see.

Escalation without context

The single-most problematic aspect of z-index as a CSS rule is the global nature of how it’s applied. Under the most ideal set or circumstances, developers would have total autonomy over all z-index instances. However, even in this scenario the developer seldom can see all CSS selectors that contain a z-index property.

Browser inspector tools are little help on this front. Tabbed CSS summaries are still specific individually highlighted DOM elements. This - like the stylesheets - provide no context for all impacted properties.

Because there is no context, developers will often set the z-index as arbitrarily high as they can to ensure their element remains king of the DOM hill. It is always painful to see index values like this:

Most browsers max out at 2147483647 (2³¹ - 1). This doesn’t seem to limit developers adding 0s or 9s to the end of these values with complete disregard to this ceiling. Reflecting the stacking context of the elements within the CSS has the potential to remove this arms race altogether.

Preprocessors to the rescue

One way to consolidate these values is via CSS preprocessors like LESS or SASS. Each offer the ability to create custom functions to dynamically generate CSS rules. In LESS the solution to our z-index problem can be simplified using the following mixin:

This mixin allows us to pass a comma separated list of selector strings and render CSS specifying the associated decreasing z-index values. Let’s break this down.

Line 37 defines our mixin and its parameters (@selectorList, @ceiling, @step) and then loops through the entirely of @selectorList. Within this loop, we define a placeholder variable @selector on line 38 to be the CSS-encoded version of the raw string from our list. This value is then interpolated on line 39 with the corresponding z-index. This mixin then recurses at the next index.

The result is the ability to specify the order of elements without any knowledge (or care) of what the resulting z-indices are. I would argue it is far easier to maintain a list like this:

@selectors:
'.foreground',
'.middleground',
...
'.background';
.mapZIndices(@selectors);

The above snippet tells me everything I need to know about where in the stack context these should be. Updating this order with new additions is trivial. Furthermore removing selectors that are no longer needed will automatically reduce the bloat of the z-index values rendered.

Summary

In the end a general best practice I like to follow is that maintained code should be semantically clear about what its purpose is. Global context values like z-index need to be centralized and this approach allows for just that.

--

--