Positioning, z-index and stacking contexts
Place elements with relative, absolute, fixed and sticky, and understand why z-index sometimes does nothing.
Most CSS screenings include at least one question about where an element ends up when you change its position value, and one about why a high z-index still sits under something else. Both come from the same small set of rules.
The five position values
- static is the default. The element sits in normal flow and ignores top, right, bottom and left.
- relative keeps the element in flow, allows offsets, and becomes the reference box for absolute children.
- absolute removes the element from flow and places it against the nearest positioned ancestor. If none exists, it uses the page.
- fixed removes the element from flow and pins it to the viewport, so it stays put while the page scrolls.
- sticky acts like relative until the page scrolls past a threshold, then sticks within its parent.
A badge in the corner of a card is the classic example. The card needs position: relative or the badge will fly to the corner of the page instead.
.card { position: relative; }
.badge {
position: absolute;
top: 8px;
right: 8px;
}Making sticky actually stick
Sticky needs two things: an offset such as top: 0, and a parent that does not clip it. A header that refuses to stick almost always has an ancestor with overflow: hidden or overflow: auto, or the parent is only as tall as the header itself, so there is nothing to scroll inside.
.site-header {
position: sticky;
top: 0;
z-index: 10;
}Why z-index 999 can still lose
Two rules explain nearly every z-index surprise. First, z-index only works on positioned elements (and on flex or grid children). Second, an element with a position and a z-index other than auto creates a stacking context, and children can only be ordered inside it. Opacity below 1, transforms and filters also create stacking contexts.
.sidebar { position: relative; z-index: 1; }
.sidebar .menu { position: absolute; z-index: 999; }
.main { position: relative; z-index: 2; }The menu stays under .main. Its 999 is compared only with other things inside .sidebar, and the sidebar as a whole is level 1, below the main area at level 2. The fix is to raise the sidebar, not the menu.
Common mistakes and a practice habit
- Forgetting to position the parent, so an absolute child escapes to the page corner.
- Adding z-index to a static element and expecting a change.
- Using fixed for a mobile footer without adding bottom padding to the page, so the last content is hidden.
- Adding a transform for animation and accidentally trapping a dropdown under a sibling.
Practice habit: once a day, take one layout you see on any site, sketch which elements are positioned and which create stacking contexts, then check your guess in the browser inspector.
More in Web and app