The `position` property and its five values control how an element is removed from or kept in normal document flow, and what its offset coordinates are relative to. Getting 'relative to what' wrong is the source of nearly every positioning bug.
csspositioninglayoutz-indexstacking-context
Knowledge Check
4 questions · pass at 70%
0/4
mediummcq
1. An element has `position: absolute` with no ancestor that has a non-static position. What is it positioned relative to?
Interview Questions
3 questions
Cheatsheet
Download-ready reference
Cheatsheet
position values — flow + offset reference point:
static (default) normal flow, offsets ignored
relative normal flow, offset from ITS OWN position; establishes positioning context for children
absolute removed from flow, offset from nearest NON-STATIC ancestor (else viewport/initial containing block)
fixed removed from flow, offset from VIEWPORT — UNLESS an ancestor has transform/filter/perspective/will-change
sticky relative until scroll threshold, then acts fixed WITHIN its containing block
#1 positioning bug: forgetting position:relative on the intended absolute-positioning parent
z-index:
Only compares WITHIN the same stacking context — never across contexts
Stacking context created by: position+z-index≠auto | opacity<1 | transform | filter | will-change | isolation:isolate
Debugging: trace ANCESTOR chain for stacking-context creators, don't just raise the number
isolation: isolate → create a clean stacking context with no other side effects
fixed gotcha: ancestor transform/filter/perspective/will-change → breaks viewport-relative fixed
sticky gotcha: ANY ancestor overflow ≠ visible → can silently break sticky
inset: 0; shorthand for top:0; right:0; bottom:0; left:0;
Use absolute/fixed for: tooltips, modals, badges, overlays (flow-independent by design)
Use Flexbox/Grid for: actual multi-item layout (don't hand-roll with absolute positioning)
position controls two things at once: whether an element participates in normal document flow, and what its top/right/bottom/left offsets are calculated relative to. Every positioning bug traces back to getting one of those two questions wrong.
position: static; /* default — normal flow, offsets ignored entirely */position: relative; /* normal flow, offset relative to its OWN normal position */position: absolute; /* removed from flow, offset relative to nearest positioned ancestor */position: fixed; /* removed from flow, offset relative to the viewport (ignores scroll) */position: sticky; /* normal flow until a scroll threshold, then acts fixed within its container */
relative — offsets from itself, still takes up space#
The element still occupies its original space in the document flow (siblings lay out as if it weren't moved) — only its painted position shifts. This is also relative's other critical job, independent of whether you use top/left at all: it establishes a positioning context for absolutely-positioned children.
absolute — the "relative to nearest positioned ancestor" rule#
.parent { position: relative; } /* establishes the positioning context */.child { position: absolute; top: 0; right: 0; } /* positioned relative to .parent, not the page */
An absolutely positioned element is removed from normal flow entirely (siblings act as if it doesn't exist) and its offsets are calculated relative to the nearest ancestor with a position other than static — if no ancestor has one, it falls all the way back to the initial containing block (roughly, the viewport/<html>). This is the single most common positioning bug: forgetting to set position: relative on the intended container, causing an absolutely positioned child to jump to a completely unexpected position relative to the page instead of the nearby parent.
fixed — relative to the viewport, with one major caveat#
.modal-backdrop { position: fixed; inset: 0; } /* covers the full viewport, ignores scroll */
fixed positions relative to the viewport and doesn't move on scroll — except when an ancestor has a transform, filter, perspective, or will-change property set, in which case that ancestor becomes the containing block instead of the viewport, and the "fixed" element scrolls along with it. This is a genuinely surprising, very commonly hit real bug: a modal or tooltip using position: fixed suddenly starts scrolling with the page because someone added transform to an ancestor for an unrelated animation.
.header { position: sticky; top: 0; } /* normal flow, then "sticks" at top:0 once scrolled to */
sticky behaves like relative until the element would scroll past its specified offset threshold, at which point it behaves like fixed — but only within the bounds of its containing block. Once the parent container scrolls out of view, the sticky element scrolls away with it rather than staying pinned forever. Requires an explicit offset (top, bottom, etc.) to actually activate — without one, sticky behaves exactly like static.
Common sticky gotcha: if any ancestor has overflow: hidden, overflow: scroll, or overflow: auto (other than the intended scrolling container), it can silently break sticky positioning, since sticky is computed relative to the nearest scrolling ancestor, and an unrelated overflow value elsewhere in the ancestor chain can become that scrolling context unexpectedly.
z-index only has an effect on positioned elements (position other than static) — or flex/grid items, which have grown to also respect z-index even without an explicit position. Critically, z-index values only compare within the same stacking context — a z-index: 9999 element trapped inside a stacking context with a low z-index is still visually behind an element with z-index: 2 in a different, higher stacking context, regardless of the raw numbers. This is the source of the infamous "why won't my z-index: 99999 show on top" bug.
What creates a new stacking context (a non-exhaustive but common list): position + z-index other than auto, opacity less than 1, transform, filter, will-change, isolation: isolate. Any of these on an ancestor creates a self-contained stacking bubble — descendants' z-index values are only meaningful relative to siblings within that same bubble, not the whole page.
The most common positioning bug by a wide margin — always double-check the intended containing element actually has a non-static position.
2. Chasing a z-index bug by raising the number instead of finding the stacking context boundary#
.tooltip { z-index: 999999; } /* still invisible — wrong problem being solved */
If a higher z-index doesn't fix a stacking issue, the actual problem is almost always that the element is trapped inside a lower stacking context created by an ancestor (an opacity, transform, or positioned+z-index ancestor) — no child z-index value can escape its own stacking context. Trace the ancestor chain for stacking-context-creating properties instead of increasing the number further.
3. position: fixed unexpectedly scrolling because of an ancestor transform#
Covered above — if a "fixed" element starts moving with scroll, check every ancestor for transform/filter/will-change/perspective, since any of them silently redefines the fixed positioning containing block.
4. position: sticky not sticking, due to an ancestor's overflow#
If sticky isn't activating, check the entire ancestor chain (not just the immediate parent) for any overflow value other than visible — a distant ancestor's overflow: hidden set for a completely unrelated reason can silently disable sticky behavior.
5. Using absolute positioning for layout that flexbox/grid would handle more robustly#
Absolute positioning removes elements from flow entirely — appropriate for overlays, tooltips, badges, modals (things genuinely meant to float independent of document flow), but a poor substitute for actual multi-item layout, which Flexbox/Grid handle far more robustly (responsive by nature, no manual pixel math, no overlap when content sizes change).
Always pair position: absolute children with an explicit position: relative (or other non-static) parent, set deliberately, not accidentally inherited from elsewhere.
Use inset: 0 as a shorthand for top: 0; right: 0; bottom: 0; left: 0; when covering a positioned parent entirely (overlays, backdrops).
When debugging z-index, trace stacking contexts, not just raise numbers — inspect each ancestor in DevTools for position+z-index, opacity < 1, transform, filter.
(tooltips, modals, badges, sticky headers) — use Flexbox/Grid for actual multi-item layout.
Reserve absolute/fixed for genuinely flow-independent elements
Test sticky with the real ancestor chain, not an isolated demo — overflow on any scroll-relevant ancestor can silently break it.
Use isolation: isolate to deliberately create a fresh stacking context when you need to contain z-index values without introducing an unwanted transform/opacity side effect.