Where an element really sits
The position property is the foundation of CSS layout control: it decides
whether an element flows normally, shifts visually, or is lifted out of flow and
pinned to a containing block. Getting it wrong produces the classic bugs — a
fixed element that scrolls, or an absolute element anchored to the wrong
parent. This reference explains all five values with their containing block,
inset behaviour, flow impact and stacking-context rules.
How it works
The top, right, bottom and left insets (and the inset shorthand) are
measured against the element’s containing block, which differs per value:
static— default; insets andz-indexare ignored, element stays in flow.relative— offset from its in-flow position; original space is preserved.absolute— removed from flow; positioned in the nearest positioned ancestor.fixed— removed from flow; positioned in the viewport (or a transformed ancestor).sticky— in flow until a scroll threshold, then sticks within its scroll container.
Establishing a stacking context matters for z-index: fixed and sticky
always create one, while relative/absolute only do so once z-index is not
auto. A transform or filter on an ancestor silently reparents fixed
descendants — the single most common positioning surprise.
Side-by-side comparison
| Value | In flow? | Containing block | Creates stacking context |
|---|---|---|---|
static | Yes | n/a (insets ignored) | Never |
relative | Yes (space preserved) | Its own in-flow position | When z-index is not auto |
absolute | No (removed) | Nearest positioned ancestor | When z-index is not auto |
fixed | No (removed) | Viewport (or transformed ancestor) | Always |
sticky | Yes (until threshold) | Nearest scroll container | Always |
Diagnosing common bugs
“My absolute element is anchoring to the page, not the container.”
The element walks up the DOM looking for the nearest ancestor with a position other than static. If no positioned ancestor exists, it falls back to the initial containing block (the document root). Fix: add position: relative to the intended parent.
“My fixed element scrolls with the page instead of staying pinned.”
A transform, filter, perspective, will-change: transform, or contain: layout on any ancestor creates a new containing block for fixed descendants, overriding the viewport. Inspect the ancestor chain for any of these properties. This is a genuine CSS specification behaviour, not a browser bug.
“My sticky element doesn’t stick.”
Check three things: the element or one of its ancestors has overflow: hidden or overflow: scroll (which prevents sticking across that boundary); the element is the same height as its containing block (there is no scroll range for it to stick within); or no inset (top, bottom, etc.) is set, which sticky requires to know where to stick.
“My z-index has no effect.”
z-index only works on positioned elements (anything other than static). If the element is static, give it position: relative without any offset — it remains in flow but now participates in stacking.
Tips and notes
- Set
position: relativeon a parent to anchorabsolutechildren to it. - A
transformanywhere up the tree can breakposition: fixed. stickyneeds room to move — it does nothing if the container is the same height.staticignoresz-index; give an element a positioning value to stack it.