---
title: 'Stacking Context'
source: 'https://academia.sh/en/courses/css-fundamentals/stacking-context'
course: 'Visual Presentation with CSS'
language: en
updated: '2026-08-17T18:09:16+00:00'
license: 'CC BY-SA 4.0'
---

# Stacking Context

The paint order of boxes; z-index values being compared only among siblings, and the declarations that establish a stacking context.

Positioning made boxes overlap: a badge sitting over a card, a stuck header over rows. One
question was left open — if two boxes overlap at the same point, which one is drawn on top?

The answer does not end with writing `z-index`. This property has a commonly encountered
behavior: sometimes a higher value ends up underneath. The reason is not a bug, it is a
structure called the **stacking context**. This lesson defines that structure.

## Default Paint Order

There is an order even when `z-index` is never written. Boxes in the same stacking context
are painted roughly in this order — what is painted first stays at the bottom:

1. The background and border of the box that establishes the context.
2. Positioned boxes with a negative `z-index` value.
3. The backgrounds of block boxes in flow.
4. Floated boxes.
5. Inline content and text.
6. Positioned boxes whose `z-index` is `auto` or `0`.
7. Positioned boxes with a positive `z-index` value.

Two observations follow from this list. First, **a positioned box is painted on top of boxes
in flow**; there is no need to write `z-index`. `position: absolute` is enough for a badge to
appear over a card.

Second, among boxes at the same level, **document order** is what decides: whatever comes
later is painted on top.

## z-index Is Compared Only Among Siblings

`z-index` does not give a box a global elevation. The value is compared only against the
box's siblings **within its own stacking context**.

When a box establishes a stacking context, every box inside it is enclosed within that
context. What gets compared with boxes outside is no longer the children's values, it is the
value of **the box that establishes the context**.

```js
// stacking.mjs — resolves stacking contexts as a tree and produces paint order
const tree = {
  name: "root", zIndex: "auto", formsContext: true, children: [
    { name: "A", zIndex: 1, formsContext: true, children: [
      { name: "A1", zIndex: 999, formsContext: true, children: [] },
      { name: "A2", zIndex: 5,   formsContext: true, children: [] },
    ]},
    { name: "B", zIndex: 2, formsContext: true, children: [
      { name: "B1", zIndex: 1, formsContext: true, children: [] },
    ]},
    { name: "C", zIndex: "auto", formsContext: false, children: [] },
  ],
};

// children inside a context are ordered by z-index, ties broken by document order
function paint(node, depth = 0, prefix = "") {
  const path = prefix ? `${prefix} > ${node.name}` : node.name;
  const z = node.zIndex === "auto" ? 0 : node.zIndex;
  console.log(`${"  ".repeat(depth)}${node.name.padEnd(4)} z=${String(node.zIndex).padEnd(4)} path=${path}`);
  const sorted = [...node.children]
    .map((c, i) => ({ c, i }))
    .sort((x, y) => {
      const zx = x.c.zIndex === "auto" ? 0 : x.c.zIndex;
      const zy = y.c.zIndex === "auto" ? 0 : y.c.zIndex;
      return zx - zy || x.i - y.i;
    });
  for (const { c } of sorted) paint(c, depth + 1, path);
  return z;
}

console.log("paint order (bottom to top, nested):");
paint(tree);

console.log("\n--- comparing A1 and B ---");
console.log("A1.z = 999, B.z = 2");
console.log("A1 is not in the root context; it is inside A's (z=1) context.");
console.log("Values compared in the root context: A=1, B=2  -> B on top.");
console.log("Result: A1 (999) stays BELOW all of B's subtree.");
```

```
paint order (bottom to top, nested):
root z=auto path=root
  C    z=auto path=root > C
  A    z=1    path=root > A
    A2   z=5    path=root > A > A2
    A1   z=999  path=root > A > A1
  B    z=2    path=root > B
    B1   z=1    path=root > B > B1

--- comparing A1 and B ---
A1.z = 999, B.z = 2
A1 is not in the root context; it is inside A's (z=1) context.
Values compared in the root context: A=1, B=2  -> B on top.
Result: A1 (999) stays BELOW all of B's subtree.
```

The last block is the exact statement of the problem this lesson solves. Box `A1`'s `z-index`
value is 999 — the highest value on the page. Despite this, it stays **below** box `B`, whose
value is 2.

The reason is that box `A1` is not in the root context. Two values are compared in the root
context: 1 for `A` and 2 for `B`. `B` wins and is painted, along with its whole subtree, on
top of `A`. Box `A1`'s value of 999 is only ever compared against its sibling `A2`.

The actionable conclusion is this: if a box is not rising as high as expected, the fix is not
to raise the value. Raising the value to a million does not help either. The fix is to
**find which box in the ancestor chain establishes a context** and make the comparison at that
level.

## What Establishes a Stacking Context

A box establishes a stacking context when one of these conditions holds:

- The document's root element (always establishes a context).
- Boxes whose `position` value is other than `static` and whose `z-index` value is other than
  `auto`.
- Boxes with `position: fixed` or `position: sticky` (even without `z-index` written).
- Boxes whose `opacity` value is less than 1.
- Boxes on which a property like `transform`, `filter`, `perspective`, `clip-path`, or `mask`
  takes a value other than `none`.
- Boxes declaring `isolation: isolate`.
- Children of a flexible box or grid layout whose `z-index` value is other than `auto`.

The middle of this list stands out. Writing `opacity: 0.99` — a change that will not be
visually noticeable — establishes a stacking context and isolates every `z-index` calculation
inside that box from the outside. The same holds for a `transform` declaration written to give
a box motion.

That a declaration with no apparent relation to stacking changes stacking looks surprising, but
there is a reason: all of these properties require the box and its content to be processed
**as a whole**. Transparency is computed by blending everything inside the box together rather
than separately, and this forces them to be treated as a single layer.

## isolation: isolate

The `isolation: isolate` declaration, the item before the last on the list, is the **side-effect-free**
way to establish a stacking context. It is written when a box's internal `z-index` values
should not leak outward, and it changes nothing else.

This is useful on pages where multiple components coexist: if each component establishes its
own context, internal `z-index` values can stay small and local. One component's `10` does not
compete with another component's `10`.

## A z-index Scale

This lesson's practical result is a writing discipline.

- `z-index` values are kept **small and named**. A page has a handful of layers — base, stuck
  header, dropdown layer, top layer — and each is given sparse values like 1, 10, 100. The
  gaps in between leave room for adding a layer later.
- Before reaching for `z-index`, it is checked whether `position` alone is enough for a box to
  stay on top; a positioned box is already above boxes in flow.
- Components are isolated with `isolation: isolate`, so their internal values do not leak
  outward.

```css
/* station.css — step 14: layer scale */
.card { position: relative; isolation: isolate; }

.card .status-badge {
  position: absolute;
  z-index: 1;
  inset-block-start: 8px;
  inset-inline-end: 8px;
}

.measurement-table thead th {
  position: sticky;
  z-index: 10;
  inset-block-start: 0;
  background-color: #f5f7f8;
}

.skip-link:focus {
  position: fixed;
  z-index: 100;
  inset-block-start: 8px;
  inset-inline-start: 8px;
}
```

The `isolation: isolate` written on the card ensures the badge's `z-index: 1` value never
competes with anything outside the card. The stuck header stays at layer 10, the focused skip
link at layer 100; the order among the three is defined for the whole page and is readable.

## Summary

- Paint order is defined even without writing `z-index`; positioned boxes are painted above
  boxes in flow, and boxes at the same level are painted in document order.
- `z-index` does not give a global elevation; it is compared only against the box's siblings
  within its own stacking context.
- When a box establishes a context, every value inside it is enclosed within that context; what
  gets compared with the outside is the value of the box that establishes the context.
- Declarations that look unrelated to stacking, like `opacity`, `transform`, and `filter`, also
  establish a context, because they require the box's content to be processed as a whole.
- `isolation: isolate` is the side-effect-free way to establish a context; it is used to keep a
  component's internal layer values local.

## Next Step

Throughout this topic, boxes were sized, arranged, and overlapped. One situation has not been
addressed yet: what happens when content does not fit its box? A long station name does not
fit a narrow cell, a wide measurement table does not fit a narrow screen. The next lesson takes
up what overflow is and what each of the clipping, scrolling, and hiding options costs.
