C.W.K.
Stream
Lesson 03 of 04 · published

position, z-index, And The Stacking Context

~12 min · position, absolute, fixed, sticky, z-index, stacking-context

Level 0Markup Novice
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"position is the escape hatch from normal flow. z-index is how things stack once you've escaped. Most layout bugs live in misunderstanding one or both."

The Five position Values

  • static — the default. The element sits in normal flow. top/right/bottom/left have no effect. z-index has no effect.
  • relative — in normal flow, but visually offset by top/right/bottom/left from its natural position. The element's reserved space is still in the original spot. Also: becomes a positioning context for absolute children.
  • absolute — removed from normal flow. Positioned relative to the nearest positioned ancestor (any ancestor with position other than static). If no positioned ancestor exists, positions relative to the initial containing block (the viewport for documents, the iframe for iframes).
  • fixed — removed from normal flow. Positioned relative to the viewport. Doesn't move when the page scrolls.
  • sticky — in normal flow until a scroll threshold is reached, then behaves like fixed within its containing block. Use for sticky headers, table headers that stay visible while scrolling rows.

The Containing Block Question

For absolute positioning, the question "positioned relative to what?" matters more than the position itself. The answer:

  • The nearest ancestor with position: relative, absolute, fixed, sticky, or transform/filter/perspective set.
  • If no such ancestor, the initial containing block (the viewport).

The canonical pattern: parent gets position: relative with no offsets, child gets position: absolute with offsets. The relative parent is the positioning context; the absolute child positions inside it.

Sticky Has An Extra Wrinkle

position: sticky sticks within its direct parent's box. It does not stick to the viewport globally — when the parent scrolls past, the sticky element scrolls away with it. This is why position: sticky on a table header works (it sticks while the table scrolls); but position: sticky inside a flex/grid child container often "doesn't work" because the parent's height doesn't allow space to scroll.

Two things sticky needs:

  1. A scroll direction defined (top: 0, bottom: 0, left: 0, etc.).
  2. A parent whose height is greater than the sticky element's reserved height (otherwise there's nowhere to stick).

z-index And The Stacking Context

When elements overlap (because they're positioned), z-index decides which is on top. Higher number wins. But — and this is the gotcha — z-index only compares elements within the same stacking context.

A new stacking context is created by:

  • position: relative/absolute/fixed/sticky WITH a z-index other than auto
  • position: fixed or sticky regardless of z-index
  • opacity less than 1
  • transform, filter, backdrop-filter
  • will-change set to a property that creates stacking context
  • isolation: isolate — the modern, intentional way
  • contain: layout, paint, or strict
  • Flexbox/grid items with z-index set

The classic confusion: z-index: 999999 on a modal doesn't put it on top of an unrelated element with z-index: 10 if the modal is trapped inside a parent that established a stacking context. Within that parent, the modal stacks correctly; outside it, the parent's stack level is what matters.

Use isolation: isolate to create stacking contexts intentionally. It's the modern, side-effect-free way to say "this region is its own z-index world." Use on the root of a component you don't want leaking z-index into its siblings, or on a region you want to protect from parent z-index pollution.

Sane z-index Layer Design

Don't pick random numbers (z-index: 9999, z-index: 999999). Define a small set of layers as custom properties and use them by name:

The Modern Centering Pattern

Centering used to require three patterns depending on what you were centering (margin auto, line-height, table-cell, absolute positioning with negative margins, transform translate). Modern CSS: display: grid; place-items: center; on the parent centers any child in both axes. display: flex; align-items: center; justify-content: center; does the same.

Pippa's Note

Cwk-site's modal layer uses isolation: isolate on the modal portal root, with z-index variables for the few layers it cares about: --z-dropdown, --z-modal-backdrop, --z-modal, --z-toast. The variables make the layering visible at the project level; isolation prevents any one component's z-index from accidentally fighting with the global stack. The classic z-index war disappears when you stop picking magic numbers and start naming layers.

Code

Each position value in practice·css
/* position: relative — keeps space, offsets visually */
.relative-demo {
  position: relative;
  top: 20px;             /* offset 20px down from natural position */
  left: 10px;            /* offset 10px right */
  /* The original 'slot' is still reserved in flow — siblings act
     as if the element is still in its natural position */
}

/* position: absolute — out of flow, positioned to nearest positioned ancestor */
.parent {
  position: relative;    /* establishes positioning context */
  width: 300px;
  height: 200px;
}
.parent .badge {
  position: absolute;
  top: 8px;
  right: 8px;            /* 8px from top-right of .parent */
}

/* position: fixed — relative to viewport, immune to page scroll */
.fab {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;         /* floating action button, always visible */
}

/* position: sticky — flows until scroll threshold, then sticks */
thead {
  position: sticky;
  top: 0;                /* sticks to top of viewport as table scrolls */
  background: white;
  z-index: 1;
}
isolation: isolate + named z-index layers·css
/* Stacking context — explicitly created with isolation: isolate */

.modal-portal {
  isolation: isolate;     /* this region is its own z-index world */
}
.modal-portal .backdrop { z-index: var(--z-modal-backdrop); }
.modal-portal .modal { z-index: var(--z-modal); }

/* z-index layers as variables — predictable, audited */
:root {
  --z-base: 0;
  --z-dropdown: 100;
  --z-sticky-header: 200;
  --z-overlay: 300;
  --z-modal-backdrop: 400;
  --z-modal: 500;
  --z-toast: 600;
  --z-tooltip: 700;
}

/* Now: */
.menu { z-index: var(--z-dropdown); }
.toast-region { z-index: var(--z-toast); }

/* Refactoring a layer means changing one variable, not 47 magic numbers */
Centering — three approaches·css
/* Centering, the modern way */

/* Grid (most concise) */
.center-with-grid {
  display: grid;
  place-items: center;   /* shorthand for align-items + justify-items */
  min-height: 100vh;     /* take up the viewport */
}

/* Flexbox (explicit) */
.center-with-flex {
  display: flex;
  align-items: center;    /* vertical */
  justify-content: center; /* horizontal */
  min-height: 100vh;
}

/* Absolute positioning (the legacy answer) */
.center-with-abs {
  position: relative;
}
.center-with-abs .child {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* Works but verbose, requires knowing the parent must be positioned,
     and the translate is a stacking-context-creator. Prefer grid/flex. */
}

External links

Exercise

Build a card with a notification badge in the top-right corner using position: relative on the card + position: absolute on the badge. Then add a sticky header to a long list. Then create a modal portal at the bottom of <body> with isolation: isolate, and verify (in DevTools' Layers panel) that the modal's stacking context is independent of anything else on the page. Finally, define five z-index layer variables and refactor any random z-index in your CSS to use them.
Hint
DevTools → Rendering (or Layers) tab shows stacking contexts. If z-index isn't working as expected, look for an ancestor that created a stacking context — opacity < 1 and transform are the usual culprits.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.