A Deep Dive into CSS Stacking Context


Introduction

When creating webpages, we typically think of them as being arranged in two directions: vertically (Y-axis) and horizontally (X-axis). This is because the default layout mode that is used by HTML and CSS is the flow layout mode. In this mode, elements are stacked either side by side (inline) or on top of each other (block-level), based on the order they appear in the HTML.

When we want to change this default behaviour, we set display properties to flex or grid and apply some other properties that determines how it should behave or use the float property.

However, there are instances where we want to overlap or stack elements on top of each other and this is where the CSS position and z-index properties amongst others come in.

Position

The position property is used to determine the positioning type of an element. It is commonly used with other properties like top, right, bottom and left to determine the location of the elements that it is applied on. With the default value of static, elements are positioned according to the normal flow of the document.

Our interest here is the absolute positioning type. This property is used to position an element relative to its closest positioned ancestor element. If there is no positioned ancestor, the document body is used instead. This value allows us to overlap elements on top of each other.

z-index

The z-index property transforms our conceptualization of a webpage from a 2-Dimensional model to a 3-Dimensional one. This property helps us determine the stacking order of elements. The higher the value of z-index, the closer the element is to the front — that is the user's viewport, the lower the value, the further the element is.

As a result of this, elements with a higher z-index value will be on top of elements with a lower z-index. The default value of z-index is 0 and it is applied on every element.

You can have a value of

  • auto which is the default value and it is calculated based on the stacking order of the elements.
  • number which is a positive or negative whole number.

Visual Representation

Before we go on, below is a visual representation of the z-index property. Each transparent blue layer represents a different z-index level. You can play around with the controls to see how the elements are stacked.

<!DOCTYPE html>
<html>
  <head>
    <title>z-index visualization</title>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="/styles.css" />
  </head>

  <body>
    <div class="controls">
      <div>
        <label for="rotateYSlider">Rotate Y:</label>
        <input
          type="range"
          min="-360"
          max="360"
          value="60"
          step="1"
          id="rotateYSlider"
        />
      </div>
      <div>
        <label for="rotateXSlider">Rotate X:</label>
        <input
          type="range"
          min="-360"
          max="360"
          value="-20"
          step="1"
          id="rotateXSlider"
        />
      </div>
    </div>
    <div class="scene">
      <div class="cube">
        <!-- Base layer -->
        <div class="layer layer-1">Base Layer (z-index: auto)</div>

        <!-- Parent stacking context -->
        <div class="layer layer-2">
          <div class="parent">
            Parent Context
            <div class="element" style="top: 10px; left: 10px;">Child</div>
          </div>
        </div>

        <!-- Higher z-index layer -->
        <div class="layer layer-3">
          z-index: 1
          <div
            class="element"
            style="top: 30px; left: 30px; background: rgba(0, 0, 255, 0.8);"
          >
            Sibling
          </div>
        </div>

        <!-- Top layer -->
        <div class="layer layer-4">z-index: 2</div>
      </div>
    </div>
  </body>
  <script src="/script.js"></script>
</html>

In the example above, we have four layers:

  • A base layer which is the lowest layer and is positioned at the bottom of the stack.
  • A parent stacking context which is the second layer and is positioned on top of the base layer.
  • A higher z-index layer which is the third layer and is positioned on top of the parent stacking context.
  • A top layer which is the fourth layer and is positioned at the top of the stacking context.

It is very important for you to note that in this example, we didn't use the z-index property to position the layers. Instead, we used the transform property.

Stacking Context

It is evident that every element in a webpage is positioned either on top of another element or underneath another element. This groups elements into what we refer to as a Stacking Context.

A new Stacking Context is formed:

  • on the root element (<html>) or
  • when an element has any of these properties:
    • position set to relative or absolute with a z-index value other than auto.
    • position set to fixed or sticky.
    • opacity less than 1.
    • transform, filter, perspective, or clip-path properties.
    • display set to flex or grid with a z-index value other than auto.
    • contain: layout or contain: paint.
    • will-change properties.
    • isolation: isolate.
    • mix-blend-mode set to anything other than normal.

This implies that every element in a webpage is a part of a default stacking context. Once a new stacking context is created, the elements within it are painted independently of elements outside the context. This means that:

  • The z-index of elements within a stacking context does not affect elements in sibling or parent stacking contexts.
  • Only the stacking context as a whole competes with other elements in the parent stacking context.

This isolation allows us to

  • carefully control rendering logic when elements overlap each other
  • update the stacking order without affecting the rest of the layout.

Earlier, I made a note that the visual representation of how elements were stacked in the z-axis wasn't done with z-index but rather with transform. This was because using the z-index in conjunction with position: absolute would have created a new stacking context.

Stacking Order

The Stacking Order determines the order in which elements are painted on the screen. This is a simplified version of the Painting Order algorithm:

  1. Start with the background and border of the stacking context itself.
  2. Move through floats, in-flow block-level and inline-level content in DOM order.
  3. Handle child stacking contexts in order:
    • Negative z-index.
    • Stack level 0 (auto or explicitly 0).
    • Positive z-index (from smallest to largest).

Things to note

  • Stacking contexts are self-contained: Child elements can never be rendered in front of their parent.
  • Each stacking context is independent: z-index values only compete within the same stacking context.
  • Once a new stacking context is created, its descendants' z-indices are only compared within that context.
  • Stacking contexts are hierarchical: Elements work their way up the tree to find the nearest stacking context.

Stacking Context in action

Now that we understand the basics of stacking context and how it affects the rendering of elements, let's see how it works in action.

Example 1

<div class="parent">
  <div class="child-1">Child 1</div>
  <div class="child-2">
    <div class="grandchild">Grandchild</div>
  </div>
</div>
.parent {
  position: relative;
  z-index: 1;
}

.child-1 {
  position: absolute;
  z-index: 2;
}

.child-2 {
  position: absolute;
  z-index: auto; /* control */
}

.grandchild {
  position: absolute;
  z-index: 999;
}

In the example above, how many stacking contexts do you think are there apart from the root stacking context? And which element shows up on top?

There are three stacking contexts:

  • The stacking context on .parent.
  • The stacking context on .child-1.
  • The stacking context on .grandchild.

The main thing to note here is that .child-2 doesn't create a new stacking context and because of this, the element that shows up on top is .grandchild as it has the highest z-index value in the .parent stacking context.

But what happens when you change the z-index of .child-2 to 1?

This creates a new stacking context and the element that shows up on top is .child-1 as it has the highest z-index value in the .parent stacking context. Remember that when a stacking context is created, its descendants' z-indices are only compared within that context.

Example 2

<div>
  <div>
    <div class="red">Red</div>
  </div>
</div>
<div>
  <div class="green">Green</div>
  </div>
  <div>
    <div class="blue">Blue</div>
  </div>
</div>
.red,
.green,
.blue {
  position: absolute;
  width: 100px;
  color: white;
  line-height: 100px;
  text-align: center;
}

.red {
  z-index: 1;
  top: 20px;
  left: 20px;
  background: red;
}

.green {
  top: 60px;
  left: 60px;
  background: green;
}

.blue {
  top: 100px;
  left: 100px;
  background: blue;
}

This example is a bit trickier. How many stacking contexts are there apart from the root stacking context? And which element shows up on top?

There is only one stacking context:

  • The stacking context on .red. This is because .red is the only positioned element with a z-index value while others have an implict z-index: auto.

This is the same element that is on top. This is because it has the highest z-index value in the root stacking context.

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="/styles.css" />
  </head>
  <body>
    <div class="container">
      <div>
        <div class="controls">
          <div>
            <label for="rotateYSlider">Rotate Y:</label>
            <input
              type="range"
              min="-360"
              max="360"
              value="60"
              step="1"
              id="rotateYSlider"
            />
          </div>
          <div>
            <label for="rotateXSlider">Rotate X:</label>
            <input
              type="range"
              min="-360"
              max="360"
              value="-20"
              step="1"
              id="rotateXSlider"
            />
          </div>
        </div>
        <div class="scene">
          <div class="cube">
            <div class="context-boundary root-context">
              <span class="context-label">Root Stacking Context (html)</span>
              <div class="context-boundary red-context">
                <span class="context-label"
                  >Red's Stacking Context (position + z-index)</span
                >
                <div class="element red">Red z:1</div>
              </div>
              <div class="element green">Green</div>
              <div class="element blue">Blue</div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>
  <script src="/script.js"></script>
</html>

What happens when you add filter: grayscale(0.8) to the parent of .red?

The element that shows up on top is .blue. This is because adding a filter to the parent of .red creates a new stacking context and its z-index gets compared within this context.

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="/styles.css" />
  </head>
  <body>
    <div class="container">
      <div>
        <div class="controls">
          <div>
            <label for="rotateYSlider">Rotate Y:</label>
            <input
              type="range"
              min="-360"
              max="360"
              value="60"
              step="1"
              id="rotateYSlider"
            />
          </div>
          <div>
            <label for="rotateXSlider">Rotate X:</label>
            <input
              type="range"
              min="-360"
              max="360"
              value="-20"
              step="1"
              id="rotateXSlider"
            />
          </div>
        </div>
        <div class="scene">
          <div class="cube">
            <div class="context-boundary root-context">
              <span class="context-label">Root Stacking Context (html)</span>
              <div class="context-boundary filter-context">
                <span class="context-label">Red's Parent (filter)</span>
                <div class="element red">Red z:1</div>
              </div>
              <div class="element blue">Blue</div>
              <div class="element green">Green</div>
            </div>
          </div>
        </div>
        <div class="structure">
          DOM Structure:
          <pre>
root stacking context
├── div (filter: grayscale)
│   └── red (z-index: 1)
├── green
└── blue</pre
          >
        </div>
      </div>
    </div>

    <script src="/script.js"></script>
  </body>
</html>

Best Practices

As you have seen, it is very easy to create numerous stacking contexts and spend a lot of time trying to debug why z-index or absolute positioning isn't working as expected and instances where setting a high value doesn't change anything.

The following are some best practices you can try out to have a more predictable stacking context setup.

Use z-index sparingly

You can try creating a scaling system for your z-index values. This way you can reduce your usage of setting abritarily high values.

:root {
  --z-negative: -1;
  --z-normal: 0;
  --z-dropdown: 100;
  --z-sticky: 200;
  --z-modal: 300;
  --z-popover: 400;
  --z-tooltip: 500;
}

Minimize New Contexts

Review your CSS styling to make sure that you create stacking contexts intentionally and not by accident.

.accidental-context {
  opacity: 0.99; /* Creates a stacking context! */
  transform: translateZ(0); /* Creates a stacking context! */
  will-change: transform; /* Creates a stacking context! */
}

Use Modern CSS properties

The isolation property is particularly useful when you want to create a new stacking context without modifying other properties:

.needs-isolation {
  isolation: isolate; /* Creates a stacking context! */
}

When using features like container queries, be aware that using some properties might cause a new stacking context to be created.

.container {
  container-type: size: /* Creates a stacking context! */
}

Summary

Understanding stacking contexts helps you provide solutions to layout issues especiallly when it comes to overlapping elements. Here are the key takeaways:

  • Every element exists within a stacking context, with the root element creating the initial one.
  • Multiple CSS properties can create new stacking contexts, not just position and z-index.
  • Stacking contexts are hierarchical - children can't escape their parent's stacking context.
  • Modern CSS provides cleaner ways to manage stacking contexts through properties like isolation.

Remember: when in doubt about why elements aren't stacking as expected, check if you've accidentally created new stacking contexts, and use your browser's DevTools to inspect the computed styles and stacking order. Microsoft Edge has a 3D view that can help visualize the stacking order.

Further Reading