z-index Not Working? The Real Reasons (Stacking Contexts Explained)
Set z-index: 9999 and the element is still behind something? z-index only works on positioned elements and only within its stacking context. The four common causes, how to spot a stacking context, and the fixes — including portals for modals and dropdowns.
You've given your dropdown z-index: 9999 and it's still hiding behind the card below it. Bigger numbers won't fix this. z-index follows rules that aren't obvious, and once you know them the fix is usually one line.
What z-index does
When elements overlap, z-index decides which appears on top. Higher numbers sit above lower ones. Simple — with two big conditions.
Cause 1: the element isn't positioned
z-index has no effect on elements with the default position: static.
.dropdown { z-index: 100; } /* ignored */
.dropdown { position: relative; z-index: 100; } /* works */
Any of relative, absolute, fixed or sticky will do. (Children of flex and grid containers are an exception: z-index works on them without position.) (CSS position explained)
Cause 2: it's trapped in a stacking context
This is the one that confuses everyone.
A stacking context is like a sealed layer. Everything inside it is stacked within that layer, and the whole layer is then stacked against its siblings as one unit. A child with z-index: 9999 can never rise above something outside its parent's layer if the parent's layer is lower.
<div class="header" style="position: relative; z-index: 1">
<div class="dropdown" style="position: absolute; z-index: 9999">…</div>
</div>
<div class="content" style="position: relative; z-index: 2">…</div>
The dropdown is stuck under .content, because its parent .header (z-index 1) is below .content (z-index 2). The 9999 only competes with other things inside .header.
Fix: raise the parent (.header { z-index: 3 }), or move the dropdown out of it.
What creates a stacking context
Not just z-index. These all create one — often by accident:
position(other than static) with az-indexvalueposition: fixedorstickyopacityless than 1transform,filter,backdrop-filter,perspective,clip-path,maskisolation: isolatewill-changeset to one of those propertiescontain: paint(orlayout)
A parent with transform: translateY(0) from an animation library or opacity: 0.99 from a fade can silently trap every child's z-index. Inspect the ancestors.
Cause 3: overflow is clipping it
Sometimes the element is on top, but it's being cut off by an ancestor with overflow: hidden (or auto). The dropdown ends at the edge of the card. That's clipping, not stacking — no z-index will fix it.
Fix: remove overflow: hidden from the ancestor, or render the element outside it (see portals below).
Cause 4: it's a different element on top
Use the browser's DevTools: right-click the spot where your element should be → Inspect. You'll see what's actually there — sometimes an invisible overlay or a transparent full-width element is sitting on top. (Browser developer tools)
Chrome and Edge DevTools also have a Layers panel that shows stacking visually.
The best fix for modals, dropdowns and tooltips: portals
Floating UI is easiest when it's rendered at the end of <body>, outside every card, transform and overflow. In React that's a portal:
import { createPortal } from 'react-dom'
return createPortal(<div className="modal">…</div>, document.body)
Component libraries like shadcn/ui and Radix do this automatically for dialogs, popovers and dropdowns — which is why they rarely have z-index problems. (What is shadcn/ui?)
Keep z-index values sane
Instead of an arms race of 999, 9999, 99999, define a small scale and stick to it:
:root {
--z-dropdown: 10;
--z-sticky: 20;
--z-modal: 30;
--z-toast: 40;
}
Checklist
- Is it positioned (or a flex/grid child)?
- Does an ancestor create a stacking context (transform, opacity, filter, z-index)?
- Is an ancestor's
overflowclipping it? - What does DevTools say is actually on top?
- For floating UI: use a portal.
EasySpawn runs your app with a live preview next to Claude Code, so you can ask "why is this dropdown hidden?" and watch the fix land. See how it works or join the waitlist.
Related: CSS Position Explained · Browser Developer Tools for Beginners · How to Center a Div · Flexbox vs Grid
Keep reading
"ReferenceError: window is not defined" in Next.js: How to Fix It
window, document and localStorage only exist in the browser. Next.js renders components on the server first, where they don't exist. Why it happens, and the fixes: useEffect, 'use client', typeof checks, dynamic imports with ssr: false, and libraries that touch window on import.
"Unexpected Token < in JSON at Position 0": What It Means and How to Fix It
Your code expected JSON and got HTML — almost always an error page or your app's index.html. Why it happens (wrong URL, 404, server error, SPA fallback, login redirect), how to see what the server actually sent, and how to parse responses safely.