CSS position values — static, relative, absolute, fixed, sticky
static = default flow. relative = in flow but offsettable + new positioning context. absolute = removed from flow, anchored to nearest positioned ancestor. fixed = anchored to viewport (or transformed ancestor). sticky = relative until a threshold, then fixed within scroll container.
position controls how an element is placed relative to its containing block and whether it participates in normal flow. Junior code uses absolute for everything; the senior signal is knowing which positioning context each value creates and the gotchas (transform containing block, sticky inside overflow:hidden).
position: static (default).
In normal flow. top/right/bottom/left/z-index are ignored. Doesn't create a positioning context for absolute children.
position: relative.
Stays in normal flow (takes its original space) but you can nudge it with top/left etc. without affecting siblings.
The big use: becomes a positioning context for position: absolute descendants. Most "absolute inside relative parent" patterns rely on this.
position: absolute.
Removed from normal flow — siblings collapse as if it weren't there. Positioned relative to the nearest positioned ancestor (any position other than static), or the initial containing block (viewport/html) if none.
Common patterns: badge in the corner of a card (position: relative parent + position: absolute badge), tooltip anchored to a trigger.
Gotcha: forget the relative parent and the absolute element pops up to the viewport — looks like a fixed element.
position: fixed.
Removed from flow. Anchored to the viewport — stays put when you scroll. Right for floating action buttons, persistent headers, modals.
The famous gotcha: fixed is contained by an ancestor with transform, filter, or will-change set. CSS spec quirk: those properties create a new containing block. So a fixed element inside a transformed parent positions relative to the parent, not the viewport. Most "why is my modal misaligned" bugs trace back to this.
position: sticky.
Hybrid: behaves like relative until the element crosses a scroll threshold, then "sticks" like fixed within its scroll container.
.section-header { position: sticky; top: 0; }Sticks to the top of its scroll container as the user scrolls past it.
Sticky gotchas:
- Containing block. Stickyness is scoped to the nearest scroll container. If no ancestor scrolls, it sticks to the viewport.
- Parent overflow.
overflow: hidden(orauto/scroll) on an ancestor cancels stickiness in older browsers. Even modern:overflow: hiddenon the containing element makes sticky useless because it can't escape. - Height. The sticky element only sticks within its parent's height. When you scroll past the parent, the sticky element scrolls away too. This is the feature, not a bug — it's what makes sectioned sticky headers work in lists.
- Threshold.
top,bottom,left, orrightdefines the sticking edge.top: 0sticks at top;top: 16pxleaves a 16px gap.
Stacking contexts. position: relative/absolute/fixed/sticky + z-index (other than auto) creates a new stacking context, which clips child z-indexes. Two elements in different stacking contexts can't interleave by z-index — root context wins. isolation: isolate creates a stacking context without needing position/z-index, useful for portals.
Decision matrix.
- Tooltip / badge / dropdown anchored to a trigger → relative parent + absolute child.
- Floating UI persistent across scroll → fixed.
- Header that sticks within a section list → sticky.
- Modal overlay → fixed (and ensure no transformed ancestor).
- Card layout → static (default flow + flex/grid).
Modern alternative: anchor positioning (anchor-name / position-anchor) is landing in browsers — Chrome 125+. Lets you position an absolute element relative to any element, not just the nearest positioned ancestor. Watch this space.
Code
Follow-up questions
- •Why does position:fixed sometimes not anchor to the viewport?
- •Why does sticky stop sticking partway down?
- •What's a stacking context?
- •How does the new CSS anchor positioning work?
Common mistakes
- •Using `absolute` without a positioned parent — pops out to the viewport.
- •Putting `fixed` inside a transformed ancestor and being surprised it doesn't anchor to viewport.
- •Setting `overflow: hidden` on a parent of a sticky element — kills the sticky.
- •Mixing z-index across stacking contexts and expecting numeric ordering.
Performance considerations
- •Sticky elements promote to a layer; cheap to keep on screen during scroll.
- •Fixed headers can avoid scroll-related repaints if marked will-change: transform.
Edge cases
- •A position:fixed iframe inside a transformed ancestor → still anchors to ancestor.
- •Sticky element with parent shorter than the element → never sticks.
- •Sticky in flexbox parent with align-items:stretch can fail to stick — set align-self: flex-start.
Real-world examples
- •Sticky table headers, sticky CTA bars, modal overlays, dropdown menus, floating chat widgets.