0% прочитано

CSS Stacking Contexts in a Vue Admin: Why a Huge z-index Was Not Enough

In the ICanUp editor, Media Explorer opened above Content Blocks, yet parts of the underlying editor could still render over the modal layer. This article explains why a huge z-index is not always enough, how CSS stacking contexts work, and why overlays need a clear layer contract instead of arbitrary numbers.

19 вересня 2026 р. 11 хв читанняCSS

In the ICanUp editor, I opened Media Explorer from a Content Block to select an image and saw a strange layering problem: the modal was open, yet parts of the underlying editor could still render above it.

At first, this looked like a simple z-index conflict.

The obvious reaction is to give the modal an even larger number.

But CSS does not use one global ranking of every element on the page. That is what makes this class of bug much more interesting.

An element with z-index: 999999 is not guaranteed to appear above an element with z-index: 10 when they belong to different stacking contexts.
CSS stacking context diagram showing Media Explorer overlay trapped inside one stacking context while Content Blocks controls render in another higher context
Media Explorer and Content Blocks may belong to separate stacking contexts. The browser compares the contexts first, not only the z-index values of their descendants.

The Symptom Looked Like a Wrong z-index

While Media Explorer is open, it should be the top interactive layer.

The following elements must not appear above it:

  • Content Block containers;
  • block headers and drag handles;
  • block action buttons;
  • sticky editor controls;
  • dropdowns and toolbars belonging to the page behind the explorer;
  • any other UI owned by the background editor.

Once any of those elements renders above the modal, the problem is no longer merely cosmetic.

The interaction hierarchy is broken: the user sees an overlay while the background UI still visually competes with it.

z-index Is Not a Global Ranking

The most important rule is that z-index is evaluated within a particular stacking system.

When two elements belong to different stacking contexts, the browser compares those contexts before comparing their descendants.

HTML - Two independent interface branches
<div class="editor">    <div class="content-block">        <button class="block-action">Edit</button>    </div></div> <div class="overlay-host">    <div class="media-explorer">        ...    </div></div>

If .editor and .overlay-host belong to separate stacking contexts, a huge z-index on .media-explorer can remain trapped inside the context created by its parent.

Imagine Two Separate Stacking Contexts

CSS - Huge z-index inside a lower context
.editor {    position: relative;    z-index: 20;} .overlay-host {    position: relative;    z-index: 10;} .media-explorer {    position: fixed;    z-index: 999999;}

It feels intuitive that 999999 should beat 20.

But if .overlay-host already forms a stacking context at level 10, its Media Explorer cannot simply jump over the sibling .editor context at level 20.

999999 remains inside its context
root stacking context ├── editor context          z-index: 20│   └── Content Block│└── overlay-host context    z-index: 10    └── Media Explorer      z-index: 999999

A child can be the highest element inside its stacking context and still remain below a neighboring context.

z-index Is Not the Only Thing That Creates a Stacking Context

This is why debugging only z-index is not enough.

Several CSS conditions can create a new stacking context.

Property or condition

Why it matters

position + z-index

The classic way to create a separate context

position: fixed

Can create its own stacking context

position: sticky

Also participates in its own stacking behavior

transform

Even a visually subtle transform can create a context

opacity below 1

Isolates descendants inside another context

filter

Can also create a stacking context

isolation: isolate

Creates one deliberately

transform Is Particularly Easy to Miss

In a complex admin interface, transform may not come from an obvious modal animation.

It can be introduced by drag and drop, a transition, a component library, or a tiny rendering optimization:

CSS - A subtle transform still has consequences
.editor-section {    transform: translateZ(0);}

The element may look completely normal, but to the browser it now creates a new stacking boundary.

That is why I inspect not only Media Explorer itself but its entire ancestor chain.

The Entire Ancestor Chain Matters

Inspecting overlay ancestors
Media Explorer    |    vModal wrapper    |    vEditor component    |    vLayout container    |    vAdmin root

Any element in that chain can create a stacking context and constrain every descendant z-index.

The same inspection is needed for the element unexpectedly rendering above the overlay.

DevTools Is More Useful Than Increasing the Number

Instead of repeating 1000 -> 10000 -> 999999, I want to answer a few precise questions first.

  • Which exact element is covering the overlay?
  • Which stacking context does it belong to?
  • Which stacking context contains Media Explorer?
  • Which ancestor created each context?
  • Which computed styles establish those boundaries?
  • Is the overlay mounted high enough in the DOM?

Browser DevTools makes it useful to inspect computed values for position, z-index, transform, opacity, filter, and isolation.

position: fixed Does Not Always Mean the Viewport

There is another trap.

position: fixed is usually associated with the viewport, so it feels natural for modal interfaces.

But certain ancestor properties, especially transform, can change the containing block used by a fixed descendant.

CSS - Fixed overlay inside a transformed ancestor
.admin-content {    transform: translate3d(0, 0, 0);} .media-explorer {    position: fixed;    inset: 0;}

Seeing position: fixed is therefore not enough. I also need to know where the element is mounted and what its ancestors are doing.

Backdrop and Dialog Should Belong to One Layer

Media Explorer is more than the visible dialog.

At minimum, there are two parts:

Modal layer structure
Overlay├── Backdrop└── Dialog

If backdrop and dialog live at unrelated levels, the result can become even stranger: the background is dimmed while a Content Block action appears above the backdrop or even above the dialog.

I want both parts to form one consistent top-level overlay stack.

A Global Overlay Should Not Be Trapped Deep Inside the Editor

One robust architectural approach for complex modal components in Vue is to render them close to the application root instead of leaving them inside whichever component opened them.

HTML - A dedicated top-level overlay host
<body>    <div id="app">        <!-- editor -->    </div>     <div id="overlay-root">        <!-- global overlays -->    </div></body>

In Vue, Teleport is often a useful tool for this.

HTML - Move Media Explorer out of a local DOM context
<Teleport to="body">    <MediaExplorer        v-if="isOpen"    /></Teleport>
Teleport is not a magical fix for every stacking bug. It does, however, help remove a global overlay from an accidental local stacking context created by the component that opened it.

I Do Not Want a z-index Arms Race Across Content Blocks

The worst direction after this kind of bug looks like this:

CSS - An arbitrary z-index arms race
.block-card {    z-index: 20;} .block-actions {    z-index: 100;} .editor-toolbar {    z-index: 500;} .media-explorer {    z-index: 999999;}

The interface gradually becomes a collection of numbers nobody understands.

The next dropdown gets 1000000, the next modal gets an even larger number, and the original structural problem remains.

What I Want Instead Is a Layer Contract

I want UI layers to have explicit responsibilities.

CSS - Example layer system
:root {    --z-base: 0;    --z-sticky: 100;    --z-dropdown: 200;    --z-overlay: 1000;    --z-modal: 1100;    --z-toast: 1200;}

The exact numbers matter less than the contract.

What matters is that the interface agrees on the hierarchy:

  • normal content lives at the base level;
  • sticky controls may sit above content but below a global overlay;
  • dropdowns have a known layer;
  • modal and backdrop own a global top layer;
  • toasts may have their own service layer above dialogs.

Media Explorer Must Be the Top Interactive Layer

The Media Explorer contract is especially simple.

While it is open, the editor behind it should not visually or interactively compete with it.

Expected interaction hierarchy
Media Explorer open Interactive:- Media Explorer dialog- Explorer folders- Explorer files- Explorer internal dropdowns- selection controls Not interactive:- Content Blocks- editor actions- sticky page controls- page dropdowns behind the overlay

pointer-events Does Not Fix the Wrong Visual Order

It is possible to disable background interaction with pointer-events, but that solves another problem.

CSS - Disable background interaction
.editor[aria-hidden="true"] {    pointer-events: none;}

If an element is still visibly rendered above the modal, pointer-events: none does not repair the layering.

It only makes the incorrectly layered element non-clickable.

Overflow Can Produce a Different Class of Visual Bug

Not every modal rendering bug is a stacking-context problem.

Sometimes the symptoms look similar while the overlay is simply being clipped by an ancestor with overflow.

CSS - An ancestor may clip a nested overlay
.editor-panel {    overflow: hidden;}

That is why I separate a few visual symptoms during debugging.

Symptom

Likely direction

Another element renders above the modal

stacking context / z-index

Part of the modal is physically clipped

overflow / containing block

A fixed overlay moves with a container

transform / containing block

The background remains clickable

pointer-events / backdrop coverage

The Fix Has to Be Verified Beyond the Post Editor

Media Explorer in ICanUp is a reusable component.

Fixing only the location where it was first noticed in the Post editor would therefore be too narrow.

If the problem belongs to the overlay contract, every editor reusing the component needs verification.

  • Post editor;
  • Page editor;
  • News editor;
  • other places using Content Blocks;
  • mobile and tablet layouts.

Media Explorer Internal Dropdowns Still Need Correct Layering

Raising Media Explorer itself must not accidentally break layers inside the explorer.

A dropdown inside the dialog needs to appear above dialog content while still remaining part of the overall modal hierarchy.

Nested layers stay inside the modal contract
Global modal layer└── Media Explorer    ├── backdrop    ├── dialog    └── internal dropdown

What I Verify After the Fix

  • No Content Block renders above Media Explorer.
  • Drag handles stay below the backdrop.
  • Sticky controls remain below the overlay.
  • The backdrop covers the intended admin viewport.
  • Scrolling does not change the layer order.
  • Opening folders does not cause layer jumping.
  • Preview and selection controls remain above dialog content.
  • Dropdowns inside Media Explorer do not disappear behind the dialog.
  • Closing the overlay restores normal editor interaction.
  • The same contract works in other editors.
  • Mobile layouts do not introduce a separate overlap bug.

Automated Tests Do Not Replace Visual Verification Here

CSS layering is difficult to prove completely with an ordinary unit test.

A focused regression test can still protect an important part of the contract: Media Explorer is mounted in the expected overlay host, the modal uses the correct layer token, and the editor does not introduce another local modal wrapper.

I still verify the actual browser rendering afterward.

What I Do Not Do After This Kind of Bug

  • Do not increase z-index blindly before inspecting stacking contexts.
  • Do not treat z-index as a global ranking for the whole page.
  • Do not inspect only the modal while ignoring its ancestors.
  • Do not forget transform, opacity, filter, and isolation.
  • Do not scatter arbitrary z-index values across individual Content Blocks.
  • Do not use pointer-events as a replacement for correct layering.
  • Do not fix a reusable Media Explorer only in one editor.
  • Do not forget to verify backdrop, dialog, and internal dropdowns separately.

My Short Checklist for Stacking Context Bugs

  • Identify the exact element covering the overlay.
  • Determine the stacking context of both elements.
  • Inspect the ancestor chain.
  • Check position and z-index.
  • Check transform.
  • Check opacity.
  • Check filter.
  • Check isolation.
  • Check overflow and containing blocks.
  • For a global overlay, consider a top-level host or Vue Teleport.
  • Use a clear layer contract instead of arbitrary numbers.
  • Verify every place where the reusable component appears.

A huge z-index does not make an element globally topmost. First you need to know which stacking context it actually belongs to.

Conclusion

The Media Explorer bug in ICanUp looked like a simple z-index problem, but it is really a problem of CSS layer structure.

If an overlay is trapped inside a local stacking context, a huge number on the dialog itself cannot guarantee that it will appear above a neighboring editor context.

That is why I inspect not only z-index, but also the complete ancestor chain, transform, position, opacity, filter, isolation, overflow, and the actual DOM placement of the overlay.

For a reusable Media Explorer, the better direction is not an arms race of numbers but a clear top-level overlay contract in which backdrop and dialog are guaranteed to sit above the whole editor.

My main lesson is simple: when z-index does not behave as expected, stop increasing the number and start looking for the stacking-context boundary.