There are several articles that explain z-index (here’s a good one), since it continues to trip up developers of all experience levels. I do not think that the number of articles is a sign that none of them do a good job at explaining it, but that there are a lot of developers out there and just because one developer read and understood the article doesn’t necessarily mean that everyone on their team read and understands it now. While taking the time to better understand how z-index (or any piece of technology) works will definitely set you up to work with it better, we can also take another approach: make it easier to work with z-index.
We use abstractions and conventions to hide away the tricky and error-prone parts, which in turn makes it easier for everyone who needs to do the same task. I had the opportunity to attempt to make z-index easier to work with for my team while working on a redesign of our company’s website. The system I designed allowed my team to implement the entire UI while never having to question what a certain z-index value was used for, what number to use when adding a new z-index declaration, or how to fix stacking bugs that crept into the system.
Common Solution
The most common system I’ve seen for managing z-index values — other than no system — is setting several general-use values, each separated by an arbitrary number. This solution definitely tames z-index issues, but as I’ve worked on teams that use this system there still seems to be confusion about how to use it properly. Here is an example from the Bootstrap documentation.
$zindex-dropdown: 1000;
$zindex-sticky: 1020;
$zindex-fixed: 1030;
$zindex-modal-backdrop: 1040;
$zindex-modal: 1050;
$zindex-popover: 1060;
$zindex-tooltip: 1070;
Copy
Bootstrap defines z-index values in Sass variables like $zindex-dropdown, $zindex-sticky, and $zindex-fixed. Those names seem pretty straight forward, but when a developer goes to choose a value for a feature they’re working on, there could be confusion as to which value is most appropriate for their use. They end up asking, “Is what I’m implementing a ‘dropdown’ or a ‘popover’?” which can easily be debated and may not have a clear answer.
A second issue I see with this solution is that the actual values for the variables might seem confusing or lead to insecurity. This solution leaves space in between each value to give developers space to add their own values in between if necessary. Bootstrap defines seven values separated by increments of 10, starting at 1000 and ending at 1070.
Many questions could come to mind when reading this:
“Why start at 1000?
“Is there anything less than 1000?”
“Where is 1010? Is it a bug? Is something else using it?”
“Why was 10 chosen? What if I need more than 9 values to go in between?”
Though I’ve never actually needed these “what if” questions answered, they can add insecurity and confusion to a system that already seems magical and misunderstood. Can we remove all of these concerns, allowing the developer to easily and accurately choose the z-index value they need?
A New Solution
Since working on a redesign gave my team a fresh start, this was one common issue we wanted to see if we could avoid. To align with our general coding standards, my goals for managing z-index was to avoid magic numbers and to make it easier for every team member to confidently contribute. The second goal of making it easier for others is vague, so I focused on trying to solve these common issues:
- People often choose arbitrarily large z-index values;
- z-index bug fixes often result in a new z-index bug;
- The relationship between z-index values is difficult to trace.
Let’s look at solutions for each of these issues that I was able to apply, leaning on conventions and using existing technologies.
Giving Z-Index Values Semantics
One reason people often choose arbitrarily large z-index values is because they don’t know the z-index value of the item above which they are trying to place a new item. Once they find an arbitrarily high value that works, they leave it instead of finding an optimal value. Later on, when someone finds this value they have no idea why it is what it is, and even the original author may have forgotten.
z-index: 9999;
Copy
The solution for fixing “magic numbers” like this is by using a named constant instead. While naming the value alone does not give us much more value than the class name does, when we put our z-index constants together, their relationship starts to become explicit.
To remove the magic numbers, I first started defining all of our z-index values in a JavaScript file. I used a JavaScript file since our application was using a CSS-in-JS solution, though this and the ideas in this article can be implemented with styling preprocessors like Sass variables as well as in CSS using custom properties.
export const backdrop = 0;
export const openButton = 1;
export const dropdown = 2;
Copy
With z-index constants, the CSS value has little more meaning, and the actual value is obscured away.
css`
z-index: ${backdrop};
`;
Copy
This also makes the original value easy to find, revealing the related constants, but there is a further improvement that can be made. We know by how z-index works that these values are related to each other, so we can change our constants to make that more apparent.
export const backdrop = 0;
export const openButton = backdrop + 1;
export const dropdown = openButton + 1;
Copy
Using simple arithmetic, we can use the previous constants to make the next constant. Taking this idea one step further to further eliminate ambiguity, I added some utility constants to make these definitions read more like a sentence.
const base = 0;
const above = 1;
export const backdrop = base;
export const openButton = above + backdrop;
export const dropdown = above + openButton;
Copy
Now when someone sees a line like z-index: ${dropdown}; they can look find the dropdown’s definition and read, “The dropdown is above the open button.”
This makes future maintenance of the constants easier. Whenever you have a new value to add, you can be confident that you are adding it to the right place.
export const backdrop = base;
export const openButton = above + backdrop;
export const dropdown = above + openButton;
export const closeButton = above + dropdown; // new
Copy
Deleting values is easy too, but you need to remember to update any other values that are dependent on it. Using JavaScript, the linter highlighted this for me.
Stacking bug tickets often show up that say something like, “the dropdown is overlapping with the button when it should be underneath.” When coming across these, the fix is as simple as swapping the relationship pointers in the definitions.
export const backdrop = base;
export const dropdown = above + backdrop;
export const openButton = above + dropdown;
export const closeButton = above + dropdown; // ???
Copy
Now that we’ve swapped the z-index order, we notice another potential bug before we even check the browser. The close button might now conflict with the open button. You can now have the necessary conversations to resolve bugs before anyone sees a problem in production.
One extra piece I found to be helpful in rare situations was a utility for placing items below others. To avoid mixing above and below, I made the rule that below should only be used for negative values.
const base = 0;
const above = 1;
const below = -1;
export const backdrop = below + dropdown;
export const dropdown = below + button;
export const button = base;
Copy
“
You can also delete and add values knowing with confidence how it will affect the other stacked elements.
Once our application ended up with a dozen or so z-index constants, though it started to become a little bit confusing having a long flat list.