How to Manage z-indexes
If you've ever needed to stack things in CSS, it's very likely that you've seen code that looks like:
.a {
z-index: 9001;
}
Here you can see the z-index is set to an obscenely large integer value.
This strategy for managing stacking order is largely based on hoping that it will work, and luck.
Unfortunately, luck isn't a strategy, and items stacked in this way often result in a cascade of changes where developers progressively increase z-indexes until things seem to work.
.b {
- z-index: 8000;
+ z-index: 9002;
}
.c {
- z-index: 8500;
+ z-index: 9003;
}
Problems
There are a few problems with this luck-based approach.
Misunderstanding Stacking Context
The first is that it reveals a fundamental misunderstanding of how z-index behaves. Many developers incorrectly believe that z-index is a global stacking order.
It's not.
z-indexes are relative to their stacking context, which means that elements within different contexts are stacked based on the stacking order of their respective contexts.
In the following diagram, the z-index values of the various nodes are included in parenthesis.

The stacking order of the nodes results in an ascending visual order of:
- B (1)
- B.2 (100)
- B.1 (200)
- A (2)
- A.1 (1)
- A.2 (2)
This goes to show that using large numbers can be misleading as lower values can still stack on top due to the relative order of stacking contexts.
Disorganized Code
The second problem with assigning large values across the codebase is that there's no central source of truth when trying to determine how things should stack.
A developer would need to audit the entire codebase for all z-index values and then tediously walk through the stacking contexts in order to determine the stacking order. As there are a lot of styles that can create stacking contexts, it's very likely that even an expereinced developer would make a mistake when trying to sort things out.
Inflexible Code
While it's not common, sometimes a stacked component gets used in more than one stacking context in a different stacking order.
With z-index values distributed across a project, it can be challening to figure out when a z-index value needs to change. Instead, you're more likely to see z-index battles, where a component is shifted back and forth because of competing priorities:
Move dropdown on top of modal
.dropdown {
- z-index: 100;
+ z-index: 300;
}
Revert: Move dropdown on top of modal
.dropdown {
- z-index: 300;
+ z-index: 100;
}
The "right" fix might be something like:
.some-other-container .dropdown {
z-index: 300;
}
but it's just about impossible to tell from the code that the z-index only needs a different value some of the time.
A better approach
There's a much better approach which addresses all of these issues, and gives us flexibility and control over stacking order, while also centralizing how elements are stacked relative to each other.
Any time a component requires a z-index, the value of the z-index MUST be a var declaration with a custom property name ending in -index.
.dropdown {
z-index: var(--dropdown-index, auto);
}
Any container that needs to order its descendants can then declare itself as a stacking context using isolation: isolate.
Note
While there are a variety of ways to declare a stacking context, using
isolation: isolatemakes the intent explicit, and searchable across a codebase.
Immediately after the isolation rule, the component can then declare an ordered list of the -index properties with their respective values.
.container {
isolation: isolate;
--dropdown-index: 0;
--modal-index: 1;
--alert-index: 2;
}
By taking this approach, it's trivially easy to renumber indexes when a new one is inserted. It also allows the same component to inherit different values in different contexts:
.some-other-container {
isolation: isolate;
--modal-index: 0;
--dropdown-index: 1;
--alert-index: 2;
}
With this approach it's unlikely that you will end up with z-index values greater than 10.
Migration
One of the major benefits to this approach is that you can adopt it right away when dealing with an existing codebase that doesn't yet have a z-index strategy.
Because the --*-index custom properties won't have been used yet, you can update existing z-index rules to use the obsolete value as a fallback:
.dropdown {
- z-index: 100;
+ z-index: var(--dropdown-index, 100);
}
Then, any new stacking contexts can make use of the custom properties:
.my-new-thing {
isolation: isolate;
--modal-index: 0;
--dropdown-index: 1;
}
