EdgeCases Logo
Feb 2026
CSS
Expert
8 min read

The 16 Ways CSS Creates Stacking Contexts

z-index: 9999 still behind? Stacking contexts are the reason. Opacity, transform, filter, and 12+ other properties silently create them—trapping your elements.

css
stacking-context
z-index
layout
debugging
edge-case
browser-internals

You set z-index: 9999 and the element still renders behind something with z-index: 1. The culprit? Stacking contexts. Each stacking context is an independent layer—z-index only compares elements within the same context. Here are all the ways CSS silently creates them.

The Mental Model

Think of stacking contexts as transparent folders. Elements inside a folder can be reordered relative to each other (via z-index), but the entire folder is positioned as a unit relative to other folders. A z-index: 9999 element inside folder A can't escape above folder B if folder B is stacked higher than folder A.

<!-- Folder A: z-index: 1 on parent -->
<div style="position: relative; z-index: 1">
  <div style="position: relative; z-index: 9999">
    I'm trapped in this stacking context!
  </div>
</div>

<!-- Folder B: z-index: 2 on parent -->
<div style="position: relative; z-index: 2">
  <div style="position: relative; z-index: 1">
    I win! My parent context is higher.
  </div>
</div>

All Stacking Context Triggers

Per the CSS spec, these properties and conditions create a new stacking context:

1. The Root Element

<html>  <!-- Always a stacking context -->

The document root is always a stacking context. All other contexts are nested within it.

2. Positioned Elements with z-index

/* Classic trigger */
.modal {
  position: absolute; /* or relative, fixed, sticky */
  z-index: 10;        /* NOT auto */
}

position: absolute/relative + z-index (not auto) creates a stacking context. This is the most well-known trigger.

3. Fixed and Sticky Positioning

/* Always creates stacking context (even without z-index!) */
.sticky-header {
  position: fixed; /* or sticky */
  /* z-index not required */
}

Unlike absolute/relative, fixed and sticky elements always create a stacking context, even without explicit z-index.

4. Flexbox/Grid Children with z-index

.flex-container {
  display: flex;
}

.flex-item {
  z-index: 1;  /* Creates stacking context WITHOUT position! */
}

Flex and grid items can use z-index directly. Setting any value (including z-index: 0) creates a stacking context—no positioning required.

5. Opacity Less Than 1

/* Surprise! This creates a stacking context */
.faded {
  opacity: 0.99;
}

Any opacity value below 1 creates a stacking context. This is why opacity transitions can unexpectedly change stacking behavior.

6. Transform (Any Value Except none)

/* All of these create stacking contexts */
.transformed {
  transform: translateX(0);    /* Even "no-op" transforms */
  transform: scale(1);
  transform: rotate(0deg);
}

Even transforms that visually do nothing (translateX(0)) create a stacking context. The scale, rotate, and translate properties (shorthand alternatives to transform) behave the same way.

7. Filter and Backdrop-Filter

.blurred {
  filter: blur(5px);           /* Creates stacking context */
  backdrop-filter: blur(10px); /* Also creates stacking context */
}

8. Perspective

.perspective-container {
  perspective: 1000px;  /* Creates stacking context */
}

.perspective-child {
  transform: perspective(500px);  /* Also creates context (via transform) */
}

9. Clip-Path

.clipped {
  clip-path: circle(50%);      /* Creates stacking context */
  clip-path: inset(10px 20px); /* Same effect */
}

10. Mask Properties

.masked {
  mask: url(mask.svg);          /* Creates stacking context */
  mask-image: linear-gradient(black, transparent);
  mask-border: url(border.png) 30;
}

11. Mix-Blend-Mode

.blended {
  mix-blend-mode: multiply;  /* Creates stacking context */
}

Any value except normal creates a stacking context.

12. Isolation

.isolated {
  isolation: isolate;  /* Explicit stacking context creation */
}

isolation: isolate exists specifically to create a stacking context without side effects. Use it when you need to control stacking but don't want visual changes from opacity/transform.

13. Will-Change (Specific Values)

.will-change {
  will-change: opacity;    /* Creates stacking context */
  will-change: transform;  /* Creates stacking context */
}

will-change hints to the browser that a property will animate. For properties that create stacking contexts (like opacity, transform), the hint alone triggers one.

14. Contain: Layout or Paint

.contained {
  contain: layout;         /* Creates stacking context */
  contain: paint;          /* Creates stacking context */
  contain: strict;         /* Includes layout + paint */
  contain: content;        /* Includes layout + paint */
}

15. Container Queries

.query-container {
  container-type: size;        /* Creates stacking context */
  container-type: inline-size; /* Creates stacking context */
}

16. Top Layer Elements

<dialog open>...</dialog>  <!-- Top layer = stacking context -->
<div popover>...</div>     <!-- When open -->

Elements promoted to the top layer (fullscreen, dialogs, popovers) and their ::backdrop pseudo-elements are stacking contexts.

The Gotcha: Animations

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.animated {
  animation: fadeIn 1s forwards;
}

/* During animation: stacking context (opacity < 1)
   After animation: stacking context persists if forwards fill mode */

Animating stacking-context-creating properties temporarily creates a context during the animation. With animation-fill-mode: forwards, it persists after.

Debugging Stacking Issues

Chrome DevTools: Layers Panel

  1. Open DevTools (F12)
  2. Press Cmd/Ctrl + Shift + P → "Show Layers"
  3. Visualize stacking contexts as 3D layers

Manual Inspection

// Find all stacking contexts on a page (rough detection)
const triggers = [
  '[style*="position: fixed"]',
  '[style*="position: sticky"]',
  '[style*="opacity"]',
  '[style*="transform"]',
  '[style*="filter"]',
  // ... more checks needed for computed styles
];

// Better: check computed styles
function createsStackingContext(element) {
  const style = getComputedStyle(element);
  
  if (style.position === 'fixed' || style.position === 'sticky') return true;
  if (style.zIndex !== 'auto' && style.position !== 'static') return true;
  if (parseFloat(style.opacity) < 1) return true;
  if (style.transform !== 'none') return true;
  if (style.filter !== 'none') return true;
  if (style.isolation === 'isolate') return true;
  // ... more checks
  
  return false;
}

Fixing Common Problems

Modal Behind Overlay

/* Problem: Modal trapped in parent's stacking context */
.parent {
  position: relative;
  z-index: 1;
}

.modal {
  position: fixed;
  z-index: 9999;  /* Won't escape parent! */
}

/* Solution: Move modal outside parent, or remove parent's context */
.parent {
  position: relative;
  /* Remove z-index, or set to auto */
}

Tooltip Behind Sibling

/* Problem: Card with transform blocks tooltip */
.card {
  transform: translateY(0);  /* Creates stacking context! */
}

.tooltip {
  position: absolute;
  z-index: 100;  /* Trapped inside .card's context */
}

/* Solution: Use isolation on tooltip's parent */
.tooltip-wrapper {
  isolation: isolate;
}

Best Practices

  • Use isolation: isolate when you intentionally want a stacking context without visual side effects
  • Minimize z-index values—if you need 9999, something is wrong with your stacking architecture
  • Document stacking contexts in your component library; note which components create them
  • Check animations—opacity and transform animations temporarily create contexts
  • Use CSS custom properties for z-index--z-modal: 100; --z-tooltip: 200;

Quick Reference Table

| Property/Condition              | Creates Context? |
|---------------------------------|------------------|
| position: fixed/sticky          | Always           |
| position: abs/rel + z-index     | Yes (not auto)   |
| flex/grid item + z-index        | Yes (any value)  |
| opacity < 1                     | Yes              |
| transform ≠ none                | Yes              |
| filter ≠ none                   | Yes              |
| backdrop-filter ≠ none          | Yes              |
| perspective ≠ none              | Yes              |
| clip-path ≠ none                | Yes              |
| mask/mask-image/mask-border     | Yes              |
| mix-blend-mode ≠ normal         | Yes              |
| isolation: isolate              | Yes              |
| will-change: opacity/transform  | Yes              |
| contain: layout/paint           | Yes              |
| container-type: size/inline     | Yes              |
| Top layer (dialog, popover)     | Yes              |

Key Takeaways

  • z-index only works within a stacking context—not globally
  • 16+ CSS properties silently create stacking contexts
  • opacity: 0.99 and transform: translateX(0) create contexts
  • Use isolation: isolate to explicitly create contexts without side effects
  • Debug with Chrome's Layers panel to visualize context hierarchy

Advertisement

Related Insights

Explore related edge cases and patterns

CSS
Surface
CSS :has() — The Parent Selector That Changes Everything
6 min
CSS
Deep
Safari Animation Artifacts: The 1px Black Line Glitch
5 min
CSS
Deep
Background Bleed: The Subpixel Rendering Bug
6 min

Advertisement