Why a Huge z-index Doesn’t Always Win

Consider a tooltip with z-index: 999999 that still ends up buried beneath a plain <header>. If z-index were a simple global ordering system, this couldn’t happen. The explanation is the stacking context: z-index never compares values across contexts, only within the same one.

A simplified mental model from image editors helps here. Grouping layers in Photoshop or Figma prevents those layers from mixing with layers not in the group. All the dog layers render above all the cat layers, even if a particular cat layer has a higher number than a particular dog layer. CSS stacking contexts work the same way. Every element’s z-index belongs to the context it participates in; relative ordering can’t spill outside that context.

By default, one root context holds all nodes. Additional contexts are created when a tag pairs position values like relative or absolute with a z-index. When you make an element with both, you both set its stacking level inside the nearest ancestor group and wrap it plus all its children into a new group.

Mapping the Bug

If a .tooltip lives inside a <main> that has position: relative; z-index: 1, then it’s only meaningful within main’s context. Its 999999 only competes against other <main> descendants. In the parent root context, the entire <main> node—children included—is compared against the header. Because <main> has a smaller z-index than the header, all of its internal content goes under the header. The children don’t get their own vote.

The simplest fix is to remove the z-index from <main> so it does not create its own context. With that, the tooltip finds itself directly inside the root context next to the header, and its huge value can do its advertised job. Note that nesting depth has nothing to do with this: contexts, not DOM depth, decide stacking order.

Beyond the Positioned Boxes

People often associate z-index with relative or absolute positioning. But a stacking context being created by a positioned element plus z-index is only the most common case—not the only one.

  • An opacity below 1
  • position: fixed or sticky
  • A mix-blend-mode other than normal
  • A z-index on a flex or grid child
  • transform, filter, clip-path, or perspective
  • will-change with values like opacity or transform
  • isolation: isolate, which is the explicit, deliberate creator.

This non-obvious list creates surprises. A component built with React or plain HTML can be broken merely by changing a parent’s will-change, since that might suddenly create a context the developer never expected. For example, a common sub-belief is that z-index only works with positioned elements. Positioned layout does use z-index, but flexbox and grid also implement it—there’s no hard dependency preventing a flex child or grid child from using z-index safely.

Why z-index Flattens Descendants

Contrary to a purely “higher number means front” interpretation, setting z-index on a header creates a group around that header’s children. The children can be internally reordered, but they can’t leave their parent group to interleave with elements outside. Those children become part of one locked unit, and that unit is compared against others at the top level.

Therefore, z-index shouldn’t only be thought of as a way to change the element’s own order—it additionally creates a boundary. This double effect is at the root of most peculiar stacking bugs.

The Purpose of isolation

isolation exists precisely to create a stacking context the clean way. It applies to static (non-positioned) elements, doesn’t force a z-index value onto an element, and does not alter how a child renders. Its single

The Hidden Mechanism of Stacking Contexts

Stacking contexts exemplify the "hidden mechanisms" that underpin CSS. Many developers build interfaces for years without ever realizing these structures exist. The danger isn't just ignorance—it’s the misalignment between your mental model and reality. When your understanding is even slightly off, it's only a matter of time before that discrepancy surfaces as a baffling layout bug.

Unlike programming languages, CSS provides no warnings or error messages. When something unexpected happens, there's no built-in next step to guide your debugging. These disruptions break your flow state and undermine confidence. This is a core reason why many front-end developers find CSS frustrating rather than enjoyable.

However, once you develop an intuition for these underlying principles, CSS transforms into a genuinely pleasurable tool. Mastering the language means understanding not just the syntax but the mechanisms that govern rendering behavior.

Building That Intuition

To help developers reach that level of fluency, there is a comprehensive self-paced online course designed to explain CSS at a deeper level. It focuses on the practical skills used daily to construct a wide range of user interfaces, moving beyond surface-level styling to genuine comprehension.

The course, called "CSS for JavaScript Developers", is available now.