Scalable z-index values without magic numbers

Most advice about z-index values comes down to making the number bigger, and that is why so many layouts sit one plugin away from breaking. I have opened enough legacy stylesheets to read z-index: 99999; for what it is: a white flag. Someone stopped working out the layout and started guessing.

In 14 years of building WordPress sites, that arms race has broken more layouts for me than real browser bugs ever did. A modal lands behind the sticky header. A tooltip disappears into the sidebar. CSS is doing exactly what it was told; the problem is that nobody decided up front what the layers are, so every one of your z-index values ends up being a guess.

The myth of the “high enough” number

Last year I reviewed a pull request with z-index: 10001; in it. I asked the developer where the number came from, and the answer was the classic one: “I wanted to be sure it was on top.”

I call this magic number fatigue. On a large project with several teams and a heavy stack of plugins, nobody knows everything that floats above the page, so a big number feels like insurance. It works about as well as fixing a leaky pipe by turning up the water pressure. Sooner or later a cookie banner or a third-party chat widget outbids you, and the stylesheet fills up with z-index values nobody wants to touch because no one knows what depends on them.

The context matters more than the value

Numbers come second. First comes the stacking context. Give an element a z-index of a billion and it still sits at the bottom if its parent has its own stacking context that ranks lower. You can be the tallest person in the basement and still be below everyone on the first floor.

I went through root-level CSS structure in my Senior Dev Guide to Root Selectors. Knowing where the global styles live is the first step out of the mess.

Tokenize the layers

Components should not carry literal numbers. Move the z-index values into CSS variables in one place, and the layering turns into something you can reason about instead of something you find out about in QA.

Since modern CSS is finally safe to use in production, there is no reason to hardcode any of it. My default setup looks like this:

:root {
  --z-base: 0;
  --z-sidebar: 100;
  --z-toast: 200;
  --z-popup: 300;
  --z-overlay: 400;
}

/* Local context anchors */
--z-bottom: -10;
--z-top: 10;

Steps of 100 leave room for the next developer to slot a layer in without reworking the scale. A secondary menu that belongs between the sidebar and the toasts becomes --z-sub-nav: 150;, and the value itself says where it sits.

Relative layering with calc()

Some elements travel together. A modal and its dark backdrop are the obvious pair. Rather than hand them two separate global tokens, tie one to the other, so the backdrop stays exactly one step behind the modal no matter how you shift the global scale later.

.modal-container {
  z-index: var(--z-popup);
}

.modal-backdrop {
  /* Locks the backdrop exactly one step behind */
  z-index: calc(var(--z-popup) - 1);
}

Managing layers inside a component

Global tokens do not belong on the internals of a component. A close button inside a popup does not need z-index: 301. If the popup already creates a stacking context, which it usually does because it has a z-index of its own, local tokens are enough.

When a container has no stacking context by default, I add isolation: isolate; to give the component a stable floor. That stops negative z-index values inside it from escaping and dropping the element behind the body background.

.custom-card {
  isolation: isolate; /* Creates a new stacking context */
}

.card-decoration {
  z-index: var(--z-bottom); /* Stays behind card content, but NOT behind the card itself */
}

If your layering work is eating your week, I can take it on. I have been working with WordPress since the 4.x days.

The z-index rules I stick to

Four rules keep a scale from drifting back into guesswork:

  • No magic numbers: a z-index that is not a variable or a calc() is a bug.
  • Think in layers: the question is not how high, it is which layer this element belongs to.
  • Enforce it in CI: a tool such as z-index-token-enforcer can flag literal values in the pipeline.
  • Keep local problems local: isolation: isolate keeps a component’s layering inside the component.

What makes z-index values work is not the numbers themselves but knowing what each one means before you type it.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.