"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/lefthave no effect.z-indexhas no effect.relative— in normal flow, but visually offset bytop/right/bottom/leftfrom 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, ortransform/filter/perspectiveset. - 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:
- A scroll direction defined (
top: 0,bottom: 0,left: 0, etc.). - 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/stickyWITH az-indexother thanautoposition: fixedorstickyregardless of z-indexopacityless than 1transform,filter,backdrop-filterwill-changeset to a property that creates stacking contextisolation: isolate— the modern, intentional waycontain: layout,paint, orstrict- Flexbox/grid items with
z-indexset
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.
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
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.