CSS Z-Index

z-index is one of those CSS properties that feels like it should be simple: “bigger number = on top.” And then you set z-index: 999999; and… nothing changes.

In this tutorial, we’ll make z-index make sense, using interactive playgrounds that show you:

  • What z-index actually controls (stacking order).
  • Why z-index sometimes does nothing at all.
  • How stacking contexts can “trap” children (the #1 cause of “z-index not working”).
  • How to build real UI patterns: dropdowns, sticky headers, modals, and tooltips.

What z-index Does (and Does Not) Do

Think of your page as a stack of transparent sheets. Each element gets painted in some order. z-index lets you influence that paint order, but only for elements that participate in stacking.

Here’s the first big concept:

  • z-index compares elements inside the same stacking context.
  • If two elements are not in the same stacking context, their z-index values don’t compete directly.

z-index Only Matters When Elements Overlap

If elements don’t overlap, you won’t see any difference. Let’s force an overlap and then change stacking.

.card-a {
  z-index: 1;
}
  
.card-a {
  z-index: 10;
}
  
.card-a {
  z-index: -1;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.stage {
  width: 520px;
  max-width: 100%;
  height: 260px;
  border: 3px solid #111;
  border-radius: 14px;
  padding: 18px;
  background: #f4f4f4;
  position: relative;
  overflow: hidden;
  font-family: ui-monospace, SFMono-Regular, monospace;
}

.card {
  width: 240px;
  height: 170px;
  border: 3px solid #111;
  border-radius: 14px;
  display: grid;
  place-items: center;
  font-size: 16px;
  position: absolute;
  top: 55px;
}

.card-a {
  left: 70px;
  background: #fff;
  transform: rotate(-4deg);
  position: absolute;
}

.card-b {
  left: 170px;
  background: #e9e9e9;
  transform: rotate(5deg);
  position: absolute;
  z-index: 2;
}
  
A (we change z-index)
B (z-index: 2)

Notice what happened:

  • With z-index: 1, A is still below B, because B is 2.
  • With z-index: 10, A jumps above B.
  • With z-index: -1, A goes behind, and might slip behind the parent’s background in some layouts (like here).

The Two Rules Most z-index Bugs Come From

Rule 1: z-index Needs a Stacking Participant

In most cases, z-index only affects elements that are positioned: position: relative;, absolute, fixed, or sticky.

There is an exception: elements that are direct children of a flexbox (display: flex;) or grid container (display: grid;) can use z-index even if their own position is static.

A very common beginner bug is: “I set z-index, but it doesn’t work!” That happens because the element is position: static (the default), and your z-index isn’t participating in the stacking the way you expect.

.badge {
  z-index: 10;
}
  
.badge {
  z-index: 10;
  position: relative;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  width: 520px;
  max-width: 100%;
  border: 3px solid #111;
  border-radius: 14px;
  padding: 16px;
  background: #f4f4f4;
  font-family: system-ui, -apple-system, sans-serif;
}

.avatar {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  border: 3px solid #111;
  overflow: hidden;
  background: #fff;
  position: relative;
}

.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.badge {
  margin-bottom: -30px;
  padding: 6px 10px;
  border: 3px solid #111;
  border-radius: 999px;
  background: #fff;
  font-weight: 700;
}

.panel {
  margin-top: 12px;
  border: 3px solid #111;
  border-radius: 14px;
  background: #e9e9e9;
  padding: 14px;
  position: relative;
  z-index: 2;
}
  
The Complete CSS Z-Index Interactive Tutorial
Badge (try to go on top)
This panel is layered above (z-index: 2). Can the badge appear above it?

In the first snippet, z-index is set but the badge is not positioned. In the second snippet, we add position: relative, and suddenly the badge can stack properly.

Rule 2: Stacking Contexts Decide Who Competes With Who

A stacking context is like a mini “universe” of stacking. Inside it, z-index values compete with each other. But outside it, they don’t.

The most important consequence:

A child’s z-index cannot escape its parent stacking context.

CSS z-index Property: The Basics

z-index takes an integer or the keyword auto.

  • auto means “use the default stacking behavior.”
  • 0 is a real layer (often useful when you need to “activate” stacking).
  • positive numbers move an element above siblings with lower z-index.
  • negative numbers move an element behind siblings (with caveats).

CSS z-index: auto

auto means the element is not assigned a custom stacking level in its context. The browser will paint it in the normal order (roughly: based on document order, plus positioning rules).

z-index:
.note {
  z-index: 0;
}
  
.note {
  z-index: auto;
}
  
*, ::before, ::after {
  box-sizing: border-box;
}
.stage {
  width: 520px;
  max-width: 100%;
  height: 260px;
  border: 3px solid #111;
  border-radius: 14px;
  padding: 18px;
  background: #f4f4f4;
  position: relative;
  overflow: hidden;
  font-family: ui-monospace, SFMono-Regular, monospace;
}
.sheet {
  position: absolute;
  inset: 40px 40px auto 40px;
  height: 170px;
  border: 3px solid #111;
  border-radius: 14px;
  background: #e9e9e9;
  display: grid;
  place-items: center;
  z-index: 2;
}
.note {
  position: absolute;
  left: 60px;
  top: 10px;
  width: 240px;
  height: 210px;
  padding: 20px;
  border: 3px solid #111;
  border-radius: 14px;
  background: #fff;
  display: grid;
  place-items: center;
  transform: rotate(-16deg);
}
  
Note (we change z-index)
Sheet (z-index: 2)

Play with the options and notice how auto behaves compared to 0 or 10. In real projects, auto is what you get when you don’t set anything.

CSS z-index Values: How Numbers Really Work

You can use any integer: -999, 0, 1, 21474. But you usually shouldn’t.

Good practice is to use small, meaningful layers, like:

  • 0 base layer
  • 10 dropdowns
  • 20 sticky header
  • 30 modal overlay
  • 40 toast notifications

This avoids “z-index wars” where one component uses 999, another uses 9999, and someone eventually types 999999999 and we all pretend it’s fine.

Interactive Layer Scale With a Slider

Let’s use a slider to change z-index live. Bigger number, higher layer (within the same stacking context).

.card-green {
  z-index: 1;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.stage {
  width: 520px;
  max-width: 100%;
  height: 260px;
  border: 3px solid #111;
  border-radius: 14px;
  padding: 18px;
  background: #f4f4f4;
  position: relative;
  overflow: hidden;
  font-family: ui-monospace, SFMono-Regular, monospace;
}

.card {
  width: 240px;
  height: 160px;
  border: 3px solid #111;
  border-radius: 14px;
  display: grid;
  place-items: center;
  font-size: 16px;
  position: absolute;
  top: 65px;
}

.card-green {
  left: 90px;
  background: #eaffea;
  transform: rotate(-6deg);
  position: absolute;
}

.card-gray {
  left: 190px;
  background: #e9e9e9;
  transform: rotate(6deg);
  position: absolute;
  z-index: 3;
}
  
Green (slider)
Gray (z-index: 3)

Notice how:

  • At z-index: 4 or higher, green goes above gray.
  • At negative values, green drops behind.

CSS z-index “Always on Top”

Here’s the honest truth: “Always on top” only exists within a stacking context. If something else is in a different stacking context that sits above yours, your huge z-index won’t matter.

Pattern 1: Always on Top Within a Component

This is the common case: a badge, tooltip, or floating button that should sit above siblings.

.badge {
  z-index: 10;
}
  
.badge {
  z-index: 10;
}

.card {
z-index: 1;
} 
*,
::before,
::after {
  box-sizing: border-box;
}

.card {
  width: 520px;
  max-width: 100%;
  border: 3px solid #111;
  border-radius: 14px;
  background: #f4f4f4;
  padding: 18px;
  font-family: system-ui, -apple-system, sans-serif;
  position: relative;
}

.media {
  height: 190px;
  border: 3px solid #111;
  border-radius: 14px;
  overflow: hidden;
  position: relative;
}

.media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.badge {
  position: absolute;
  top: 12px;
  left: 12px;
  padding: 6px 10px;
  border: 3px solid #111;
  border-radius: 999px;
  background: #fff;
  font-weight: 800;
}

.overlay {
  position: absolute;
  inset: auto 0 0 0;
  height: 64px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  display: grid;
  place-items: center;
  font-weight: 700;
  z-index: 5;
}
  
The Complete CSS Z-Index Interactive Tutorial
Badge
Overlay (z-index: 5)

In the first snippet, the badge tries to be on top with z-index: 10. If it doesn’t win, it’s usually because the sibling has its own stacking behavior. In the second snippet, we explicitly set the base layer of the card to help keep ordering predictable.

Pattern 2: A Real Page Overlay (Modal)

A modal typically wants to sit above everything. The safest approach is:

  • Use position: fixed
  • Place it near the end of the document (so it’s not trapped inside a component)
  • Give it a sensible z-index layer that beats your header and dropdowns
.modal {
  z-index: 30;
}
  
.modal {
  z-index: 5;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.page {
  width: 520px;
  max-width: 100%;
  height: 300px;
  border: 3px solid #111;
  border-radius: 14px;
  overflow: hidden;
  background: #f4f4f4;
  position: relative;
  font-family: system-ui, -apple-system, sans-serif;
}

.header {
  position: sticky;
  top: 0;
  padding: 12px 14px;
  background: #fff;
  border-bottom: 3px solid #111;
  z-index: 20;
  font-weight: 800;
}

.content {
  padding: 14px;
  line-height: 1.5;
}

.modal {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
}

.modal__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
}

.modal__panel {
  position: relative;
  width: min(410px, 92vw);
  border: 3px solid #111;
  border-radius: 14px;
  background: #fff;
  padding: 14px;
}

.modal__panel h4 {
  margin: 0 0 8px 0;
  font-size: 18px;
}
  
Sticky Header (z-index: 20)

Scrollable-ish content area. In a real page, the modal should sit on top of everything.

Switch snippets to see what happens if the modal z-index is too low.

If the modal’s z-index is 5 and your sticky header is 20, the header wins and appears on top.

CSS z-index Not Working: The Real Reason (Stacking Contexts)

If you remember only one thing from this entire tutorial, remember this:

z-index is not global. It is local to a stacking context.

What Creates a Stacking Context?

Many properties can create a stacking context. Here are the most common ones you’ll bump into:

  • position + z-index (e.g. position: relative and z-index: 0)
  • opacity less than 1 (e.g. opacity: 0.99)
  • transform (even transform: translateZ(0))
  • filter (like filter: blur(0))
  • isolation: isolate
  • will-change (often when it includes transform)

This is why z-index bugs appear after “innocent” changes like adding transform for animation.

Demo: A Child Cannot Escape a Parent Stacking Context

In this demo, we’ll try to make a dropdown appear above a neighboring panel. The dropdown has a giant z-index… but it’s trapped.

.sidebar {
    isolation: isolate;
}
.dropdown {
  z-index: 999;
}
  
.sidebar {
  transform: translateZ(0);
}
.dropdown {
  z-index: 999;
}
  
.sidebar {
  transform: translateZ(0);
}
.sidebar {
z-index: 11;
} 
*,
::before,
::after {
  box-sizing: border-box;
}

.layout {
  width: 520px;
  max-width: 100%;
  border: 3px solid #111;
  border-radius: 14px;
  overflow: hidden;
  background: #f4f4f4;
  font-family: system-ui, -apple-system, sans-serif;
}

.grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  min-height: 260px;
}

.sidebar,
.main {
  padding: 14px;
  position: relative;
}

.sidebar {
  background: #fff;
}

.main {
  background: #e9e9e9;
  border-left: 3px solid #111;
  z-index: 10;
}

.button {
  display: inline-block;
  padding: 8px 10px;
  border: 3px solid #111;
  border-radius: 10px;
  background: #f4f4f4;
  font-weight: 800;
}

.dropdown {
  position: absolute;
  left: 14px;
  top: 58px;
  width: 320px;
  border: 3px solid #111;
  border-radius: 14px;
  background: #fff;
  padding: 10px;
}

.dropdown p {
  margin: 0;
  line-height: 1.35;
}
  

This panel has z-index: 10. If the left side creates a stacking context that sits below this panel, the dropdown can’t escape.

What to observe:

  • In the first snippet, we set .dropdown { z-index: 999; }. It still fails to overlap the right panel, due to isolation: isolate on the parent, which creates a new stacking context.
  • In the second snippet, we add transform to .sidebar. That also creates a stacking context, which can change who competes with who.
  • In the third snippet, we give the sidebar a higher z-index too. Now the whole sidebar stacking context can sit above the right panel, so the dropdown can finally overlap.

The key lesson: If a child needs to overlay something outside its parent, you often must raise the parent (or move the child elsewhere).

Learn even more about stacking contexts in the CSS Stacking Context Interactive Tutorial.

How to Fix z-index Not Working: A Debugging Checklist

When z-index “doesn’t work,” go through this list like a detective with a magnifying glass.

Step 1: Are the Elements Positioned?

  • Does the element have position: relative, absolute, fixed, or sticky?
  • If not, try adding position: relative first.

Step 2: Are the Elements in the Same Stacking Context?

  • If they’re in different stacking contexts, z-index values don’t directly compete.
  • Check parents for transform, opacity, filter, isolation, or z-index + positioning.

Step 3: Is an Ancestor Clipping the Overlay?

Sometimes it’s not even z-index. It’s overflow: hidden. If a parent clips overflow, your dropdown can’t visually escape.

.shell {
  overflow: hidden;
}
  
.shell {
  overflow: visible;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.shell {
  width: 520px;
  max-width: 100%;
  border: 3px solid #111;
  border-radius: 14px;
  background: #f4f4f4;
  padding: 16px;
  font-family: system-ui, -apple-system, sans-serif;
  position: relative;
}

.anchor {
  position: relative;
  height: 140px;
  border: 3px dashed #111;
  border-radius: 14px;
  background: #fff;
  padding: 12px;
}

.dropdown {
  position: absolute;
  left: 12px;
  top: 88px;
  width: 260px;
  border: 3px solid #111;
  border-radius: 14px;
  background: #e9e9e9;
  padding: 10px;
  z-index: 10;
}
  
Parent box

If your overlay is being cut off, look for overflow: hidden (or clip-path) on ancestors. That’s not a stacking problem, it’s a scissors problem.

Step 4: Is Your Overlay Inside a Transformed Ancestor?

This is sneaky: adding transform to a parent can create a stacking context, and it can also change how position: fixed behaves (it may become “fixed to that ancestor” in practice).

If a modal or tooltip behaves strangely, check for transforms on ancestors.

Common UI Recipes With z-index

A reliable dropdown usually follows a simple strategy:

  • Put the dropdown inside a relatively positioned wrapper.
  • Give the dropdown a higher z-index than nearby siblings.
  • Make sure no parent stacking context traps it under the thing it must overlap.
.dropdown {
  position: absolute;
  z-index: 10;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.frame {
  width: 520px;
  max-width: 100%;
  border: 3px solid #111;
  border-radius: 14px;
  overflow: hidden;
  background: #f4f4f4;
  font-family: system-ui, -apple-system, sans-serif;
}

.toolbar {
  padding: 12px;
  border-bottom: 3px solid #111;
  background: #fff;
  position: relative;
}

.menu {
  position: relative;
  display: inline-block;
}

.button {
  padding: 8px 10px;
  border: 3px solid #111;
  border-radius: 10px;
  background: #f4f4f4;
  font-weight: 800;
}

.dropdown {
  position: absolute;
  top: 46px;
  left: 0;
  width: 220px;
  border: 3px solid #111;
  border-radius: 14px;
  background: #fff;
  padding: 10px;
}

.content {
  padding: 12px;
  display: grid;
  gap: 12px;
}

.card {
  border: 3px solid #111;
  border-radius: 14px;
  background: #e9e9e9;
  padding: 14px;
  position: relative;
}
  
Content card below. If it gets a higher stacking layer, it could cover the dropdown.
Another card.

Sticky Header Over Page Content

A sticky header often needs a z-index so it doesn’t get covered by content scrolling underneath.

.header {
  z-index: 20;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.box {
  width: 520px;
  max-width: 100%;
  height: 280px;
  border: 3px solid #111;
  border-radius: 14px;
  overflow: auto;
  background: #f4f4f4;
  font-family: system-ui, -apple-system, sans-serif;
}

.header {
  position: sticky;
  top: 0;
  padding: 12px 14px;
  background: #fff;
  border-bottom: 3px solid #111;
  font-weight: 900;
}

.list {
  padding: 14px;
  display: grid;
  gap: 10px;
}

.item {
  border: 3px solid #111;
  border-radius: 14px;
  background: #e9e9e9;
  padding: 12px;
}
  
Sticky Header
Scroll area item
Scroll area item
Scroll area item
Scroll area item
Scroll area item
Scroll area item
Scroll area item

In real pages, other positioned elements can overlap your sticky header. Giving the header a z-index is often the correct fix.

z-index Gotchas and Common Mistakes

Using Huge Numbers Everywhere

Huge numbers don’t solve stacking contexts. They only win against siblings in the same context. If you’re “forced” to use huge numbers, it’s usually a sign your layers aren’t organized.

Negative z-index and Vanishing Elements

Negative z-index can put an element behind its parent’s background or behind other content in surprising ways. If something disappears after you set a negative z-index, check whether it’s now behind a background or clipped area.

Thinking z-index Fixes Overflow Clipping

It does not. If a parent clips, your overlay is clipped. Fix the clipping or move the overlay elsewhere (often to a higher-level container).

Quick Mental Model

When z-index feels confusing, repeat this mantra:

  • Am I overlapping? If not, you won’t see anything.
  • Am I positioned? If not, z-index may not apply as expected.
  • Am I in the same stacking context? If not, my z-index might be irrelevant.
  • Is something clipping me? If yes, I can’t escape visually.

CSS Z-Index Summary

  • CSS z-index property controls stacking order, not layout.
  • CSS z-index auto is the default behavior (no custom stacking level).
  • CSS z-index values are integers; use a small, consistent scale.
  • CSS z-index always on top only works inside the same stacking context (or when you place overlays at the right level).
  • CSS z-index not working is usually a stacking context issue: the child can’t escape the parent’s stacking context.