Complete CSS Course Module 7: Intermediate: Positioning and Layering

z-index and Stacking Order Explained

  • Beginner
  • Lesson 7.2
  • 5 min read
  • 29 of 77 in the course

In short

The CSS z-index property controls which element appears on top when elements overlap. This lesson explains stacking order and stacking contexts in simple words, and shows how to fix "z-index not working". What is z-index? A web page is not only flat.

The CSS z-index property controls which element appears on top when elements overlap. This lesson explains stacking order and stacking contexts in simple words, and shows how to fix "z-index not working".

What is z-index?

A web page is not only flat. When elements overlap, the browser must decide which one is in front. Imagine sheets of paper on a desk: the x-axis goes left to right, the y-axis goes top to bottom, and the z-axis comes out of the screen towards you. z-index sets an element's place on that z-axis.

A higher z-index comes to the front. A lower or negative one goes to the back.

.dropdown {
  position: absolute;
  z-index: 10;
}

Common mistake: Putting z-index on a normal, non-positioned element. z-index only works on elements with a position other than static, or on flex and grid items. If nothing happens, check that the element has position: relative (or absolute, fixed, sticky).

How does the browser stack elements by default?

Without any z-index, the browser paints things in this order, from back to front:

  1. The background and borders of the parent.
  2. Normal, non-positioned elements, in the order they appear in the HTML.
  3. Positioned elements (relative, absolute and so on), in HTML order.

So by default, a positioned element appears in front of a non-positioned one, and between two positioned elements, the one that comes later in the HTML wins.

Let's see three overlapping cards:

<div class="stack">
  <div class="box red">Red</div>
  <div class="box blue">Blue</div>
  <div class="box green">Green</div>
</div>
.stack {
  position: relative;
  height: 220px;
}

.box {
  position: absolute;
  width: 140px;
  height: 140px;
  color: white;
  font-weight: bold;
  padding: 10px;
  border-radius: 10px;
}

.red   { background: #dc2626; top: 0;    left: 0; }
.blue  { background: #2563eb; top: 40px; left: 60px; }
.green { background: #16a34a; top: 80px; left: 120px; }

.red {
  z-index: 3;
}

Without the last rule, green is in front because it comes last in the HTML. With z-index: 3 on the red box, red jumps to the front, even though it comes first. Blue and green keep their normal order behind it.

What is a stacking context?

This is the part that confuses most people. A stacking context is a group of elements that are stacked together as one layer. Think of it like a folder of papers: you can reorder papers inside the folder, but the whole folder sits at one spot in the pile. A paper inside a bottom folder can never go above a different folder that is on top, no matter its number.

An element creates a new stacking context when, for example, it has:

  • position: relative or absolute and a z-index other than auto.
  • position: fixed or sticky.
  • opacity less than 1.
  • A transform, filter or isolation: isolate.
  • Is a flex or grid item with a z-index other than auto.

Here is the classic "z-index not working" problem:

<header class="header">
  <div class="menu">Dropdown menu</div>
</header>
<section class="hero">Hero banner</section>
.header {
  position: relative;
  z-index: 1;
  background: #1f2937;
  color: white;
  padding: 16px;
}

.menu {
  position: absolute;
  top: 100%;
  left: 16px;
  z-index: 9999;
  background: white;
  color: #1f2937;
  padding: 16px;
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.2);
}

.hero {
  position: relative;
  z-index: 2;
  background: #fde68a;
  height: 200px;
  padding: 16px;
}

The dropdown menu has z-index: 9999, yet it is hidden behind the yellow hero. Why? The menu lives inside .header, which is a stacking context with z-index: 1. The hero has z-index: 2. The whole header "folder" is below the hero, so the menu's 9999 only counts inside the header. To fix it, give .header a higher z-index than .hero (for example z-index: 3), or remove the z-index from .hero.

How to fix z-index not working

When an element refuses to come to the front, check these things in order:

  1. Does the element have a position other than static (or is it a flex or grid item)?
  2. Is a parent creating a stacking context with a low z-index? Raise the parent, not the child.
  3. Does a parent have opacity, transform or filter? These create a stacking context too.
  4. Is a parent cutting it off with overflow: hidden? That is clipping, not stacking, and z-index cannot fix it.

Tip: Use a small, planned scale instead of random numbers like 9999. For example: 1 for raised cards, 10 for dropdowns, 100 for sticky headers, 1000 for modals. Custom properties make this easy: --z-modal: 1000;.

Note: You can create a clean stacking context on purpose with isolation: isolate;. It keeps the z-index values of a component's children from leaking out and mixing with the rest of the page. Read more on MDN's stacking context page.

Practice exercise

Create a photo with a caption box that overlaps its bottom edge, and a tooltip that appears above both when you hover a small "i" icon. Make sure the tooltip is always on top.

Hint: give the photo wrapper position: relative, the caption position: absolute with z-index: 1, and the tooltip z-index: 2. Keep them all inside the same stacking context.

Key takeaways

  • z-index controls which overlapping element is in front; higher numbers come forward.
  • It only works on positioned elements and on flex or grid items.
  • Without z-index, positioned elements are in front, and later HTML wins.
  • A stacking context groups elements; a child can never escape its parent's layer.
  • opacity, transform and filter also create stacking contexts.