The z-index Arms Race
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 0
s or 9
s 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.