Why Z-Index Still Causes Pain
Most developers have read an article or two explaining how z-index works, yet stacking bugs continue to appear in production. The problem may not be a lack of understanding so much as a lack of usable conventions. Even when a team understands stacking contexts, the process of picking a value, fixing a bug, or tracing relationships between layers often ends up being guesswork.
A common attempt at taming z-index is defining a set of named values with wide gaps. As seen in Bootstrap, these are usually abstract labels like $zindex-dropdown or $zindex-sticky. While this system provides names instead of numbers, it still leaves room for ambiguity. A developer implementing a new UI element may wonder which label fits best, and the gaps in values like 1000 to 1070 invite questions: Why start at 1000? What about the missing numbers in between? Why are the increments a specific size? These uncertainties make the system feel arbitrary and fragile even when it works.
Rethinking the Approach
A naming scheme is only part of the solution. Z-index values need to be treated like any other constant, and must also carry semantic meaning about where an element sits relative to its peers. Three recurring problems can be targeted with conventions:
- Developers tend to choose unnecessarily high numbers.
- Fixing one stacking conflict often creates another.
- The relationship between stacked items is difficult to trace.
These issues can be handled with existing technologies and conventions—no need to reinvent CSS or the tools around it.
Turning Numbers Into Relationships
Arbitrarily large values are common because a person rarely knows the exact value of the item they want to place above. Once something works, it stays. To avoid magic numbers, the answer is to define named constants for every stacking value. Naming the number alone doesn't add much insight, but when constants are grouped together, their relationships become visible.
In an application using CSS-in-JS, this can be done in a JavaScript file. The same technique works in Sass variables or CSS custom properties. With constants, the declaration has a name behind it, yet the actual value remains unknown to anyone reading it. To go a step further, constants can be derived from one another using simple arithmetic, so that the definition of each value expresses its position relative to adjacent items. For example, a summary next to a constant could read: "The dropdown is above the open button."
An additional utility, below, proves useful for negative values. A rule should be enforced that this helper is only used when dealing with negative z-index values, keeping the two directions separate and clear.
Maintaining Constants, Avoiding New Bugs
This approach makes common maintenance tasks predictable. When adding a new element, the correct place in the sequence is easy to find. When deleting a constant, dependent values must be updated as well—in a JavaScript setup, the linter will catch any trailing references. A traceable relationship also helps with bug tickets. If a dropdown is incorrectly overlapping its open button, the fix is simply a matter of swapping the relationship pointers in the definitions.
This swap can reveal follow-on conflicts before production sees them, like a close button that now clashes with the open button, giving the team the opportunity to resolve the issue in discussion rather than in an urgent hotfix.
Organizing by Stacking Context
Stacking contexts behave much like other scoped mechanisms in front-end development, akin to JavaScript modules, atomic design, or BEM. All of these tools help isolate concerns so that elements don't affect one another unintentionally. Borrowing this logic, a naming convention for constants can be established using a template like z<Context><Element>.
The z prefix identifies the constant as one for z-index, which is helpful when styling constants such as colors live in the same file. The middle portion, <Context>, is the stacking context the value belongs to, usually matching the component name. The final portion, <Element>, names the specific item being positioned within that context.
Faster Glance Inspection
This convention makes a flat list of constants much easier to read at a glance. Related values appear together, pointing out exactly where a new addition or a fix belongs. If all constants have been collated in one file, the entire stacking order of an application can be reviewed by scanning that file, which also helps surface potential conflicts early in development.
Further Maturation
What works for a single application is not necessarily the final form. Since all derived values could be computed at compile time, the runtime impact can be minimized. A build tool like Webpack or PostCSS could replace the constants with their literal values, and an implementation in Sass would naturally never ship the variables to the client.
The original design centralized all z-index values in one file alongside colors and typography. That made it easy to review but meant the flat list grew. Collocating constants by component offers an alternative—it brings each set of z-index values closer to where they are used, which is generally considered a benefit in maintainability. The final choice often depends on whether a team values a global overview or local cohesion.
A developer-experience improvement is also possible. Flat lists can become harder to manage, while Sass maps or JavaScript objects provide a hierarchical alternative. With a single z object, imports become shorter. Further organization by stacking scope could allow usage like z.layout.navigation. This style may benefit usability, although a simple flat list with a solid naming convention remains a perfectly capable system for most cases.
Outcomes From the Field
The system shipped to production as designed. The most visible win: magic numbers are gone. Developers on the team could add or adjust z-index values and fix bugs—both before and after launch—without worrying that the change would break something elsewhere. In several cases, issues were corrected before anyone even filed them.
The dreaded z-index: 9999; pattern never appeared. Even with sticky headers, floating action buttons, dropdowns, modals, pop-up ads, and other elements in the stacking context, the largest raw value in the codebase was 5—and no one had to see it, because every value lived behind a named variable.
Solving developer experience issues resulted in a z-index mini-framework, helping people make the correct decision with less effort and move more quickly.
One unexpected observation: the team sometimes assigned a z-index when one wasn't strictly required. Properties like position: sticky; can create a stacking context all on their own, similar to an explicit z-index: 1;. Even so, the team kept adding the declarations anyway. Uniformity beat minimalism—allowing exceptions would have eroded trust in the system. A complete constant list made the model easier to reason about and simplified rearranging values when the UI demanded it.
Limits of the Approach
Naming remains genuinely difficult, and this framework asks you to name every z-index value. The added naming burden was worth the clarity it bought, but it is still a chore. The system also won't point you to the stacking context where a bug lives. If you hit an unknown context or a mysteriously set value, you'll still need to hunt that down—but once you've found it, the fix path is unambiguous.
Adopting It Elsewhere
Most applications can adopt this pattern, given normal styling tooling and browser support. Migration is low-risk because stacking contexts are already scoped per element—you can move one context at a time without disturbing the rest. The process essentially forces you to articulate what already exists in the app, which often reveals corners that previously seemed messy.
If your values are a mess, the straightest route to conversion is a single file with a single constant list—one entry per raw z-index value currently in use. As stacking contexts become clearer, group and rename constants to fit the naming scheme. The team didn't use any third-party CSS libraries with their own z-index rules, but well-canvas utilities should still accommodate external values if they surface.
The examples so far show a central file of z-index constants, but colocating them with the component is viable too. A consistent file naming convention across components makes every value findable wherever it lives.
Trying It Yourself
This mini-framework came together over only a few months and has seen one production codebase. If your own z-index management is out of hand, implementing these ideas and reporting back is welcome—unexplored use cases and better opinions are still open.



