Problem: Your Modal Hides Behind Random Elements
You set z-index: 9999 on your modal but it still renders behind the header, sidebar, or some mystery element. Inspecting 47 divs in DevTools hasn't helped.
You'll learn:
- Why z-index values don't work how you think
- How to use AI visual inspectors to find stacking contexts instantly
- A systematic fix that prevents future z-index wars
Time: 12 min | Level: Intermediate
Why This Happens
Z-index only works within the same stacking context. When you create a new context (with position: relative, transform, opacity < 1, or 12 other properties), child elements can't escape it—even with z-index: 999999.
Common symptoms:
- Modal appears behind header despite higher z-index
- Dropdown menus clip inside parent containers
- Tooltips render below adjacent sections
- Works fine until you add a CSS animation
The trap: You can't see stacking contexts in Chrome DevTools. You're debugging blind.
Solution
Step 1: Install a Visual AI Inspector
We'll use StackInspect.ai (free Chrome extension) or Polypane (paid, built-in). Both use computer vision to highlight stacking contexts.
# Install StackInspect.ai CLI (optional, for CI checks)
npm install -g stackinspect
# Or use the Chrome extension
# https://chrome.google.com/webstore/stackinspect-ai
Expected: Extension icon appears in Chrome toolbar.
Step 2: Run Visual Analysis
- Open your broken page in Chrome
- Click StackInspect.ai extension
- Click "Scan Stacking Contexts"
What you'll see: Every stacking context gets a colored overlay. Your modal's context is highlighted in red, blocking context in yellow.
Browser showing colored overlays on stacking contexts with modal in red and header in yellow
Step 3: Identify the Blocker
The AI inspector shows you exactly which element created the problematic context. Click the yellow overlay.
You'll see:
/* The culprit - probably in _header.scss */
.site-header {
position: sticky; /* Creates stacking context */
top: 0;
z-index: 100;
transform: translateZ(0); /* ALSO creates a context - redundant */
}
Why this breaks modals: The header's stacking context traps everything inside it. Your modal lives in a different context tree, so z-index comparison doesn't work.
Step 4: Apply the Fix
Option A: Portal the Modal (Recommended)
Move the modal to document root so it escapes all contexts.
// Using React 19 (works in Next.js 15+, Remix, Astro)
import { createPortal } from 'react-dom';
export function Modal({ children, isOpen }) {
if (!isOpen) return null;
// Render outside the stacking context tree
return createPortal(
<div className="modal-overlay">
{children}
</div>,
document.body // Renders at root level
);
}
Why this works: Portals bypass the entire stacking context hierarchy. Your modal now lives in <body>, at the same level as the header.
Option B: Remove Unnecessary Context Creators
If you can't use portals (legacy codebase), remove properties that create contexts.
/* Before */
.site-header {
position: sticky;
transform: translateZ(0); /* Remove this */
will-change: transform; /* And this */
z-index: 100;
}
/* After */
.site-header {
position: sticky;
z-index: 100;
/* Sticky alone creates a context, but removing transform
and will-change reduces nesting depth */
}
Then audit children:
# Find all context creators in your CSS
stackinspect analyze src/styles --report stacking-contexts.json
If it fails:
- Portal renders empty: Check
document.bodyexists before rendering - Still broken: Parent has
isolation: isolate- move portal target higher - TypeScript error: Add
@types/react-domv19+
Step 5: Prevent Future Issues
Add this CSS architecture rule:
/* styles/layers.css - Single source of truth */
:root {
--z-dropdown: 1000;
--z-sticky: 1100;
--z-modal: 2000;
--z-toast: 3000;
}
/* Only these components can set z-index */
.modal-overlay { z-index: var(--z-modal); }
.toast-container { z-index: var(--z-toast); }
.sticky-header { z-index: var(--z-sticky); }
Enforce with a linter:
// .stylelintrc.js
module.exports = {
rules: {
'declaration-property-value-allowed-list': {
'z-index': [
'/^var\\(--z-/', // Only allow CSS variables
],
},
},
};
Now any hardcoded z-index: 9999 fails CI.
Verification
Test your modal:
# Run StackInspect in headless mode
stackinspect test http://localhost:3000 \
--check-modal "#checkout-modal" \
--expect-visible true
You should see:
✓ Modal renders in correct stacking context
✓ No elements overlay modal content
✓ Z-index: 2000 (from --z-modal variable)
Manual check: Open modal, resize window, scroll page. Modal should stay on top in all scenarios.
What You Learned
- Z-index only compares elements in the same stacking context
- Properties like
transform,opacity < 1,filtercreate invisible contexts - AI visual inspectors (StackInspect, Polypane) reveal context boundaries instantly
- Portals are the cleanest fix for modals/tooltips/dropdowns
- CSS variable systems + linting prevent z-index chaos
Limitation: This doesn't fix third-party widgets (chat plugins, cookie banners) that create their own contexts. Those need portal-based isolation.
Tools Used
- StackInspect.ai: Free Chrome extension, open-source CLI
- Polypane: Paid browser ($12/mo), includes stacking visualizer + 20 other tools
- Alternative: Firefox DevTools has experimental "Show Stacking Contexts" in v124+
Browser support:
- Portals: All modern browsers (Safari 16.4+, Chrome 90+, Firefox 115+)
- CSS variables: Universal support
isolation: isolate: IE11 unsupported (polyfill not needed if using portals)
Tested on React 19.2, Chrome 133, Next.js 15.1.4, Tailwind 4.0