Positioning
CSS positioning removes elements from normal flow to place them at explicit coordinates — four values with distinct behavior that learners frequently confuse.
- Describe the behavior of all five position values
- Explain the role of the containing block for absolutely positioned elements
- Use position relative on a parent to anchor an absolutely positioned child
- Distinguish between position fixed and position sticky
- Avoid the common mistake of position absolute escaping to an unexpected ancestor
Flexbox and Grid arrange groups of related items. Sometimes you need something different: a modal overlay that covers the entire screen, a tooltip that floats above its trigger, a header that stays fixed at the top while the page scrolls. For those situations, CSS positioning removes an element from the normal flow arrangement entirely and lets you place it with explicit coordinates.
position: static (the default)
Every element starts with position: static. It participates in normal flow; the
top, right, bottom, and left properties have no effect. You will rarely
write this explicitly — it is the value you are canceling when you set anything else.
position: relative
A relatively positioned element stays in normal flow — it occupies its usual place
and other elements lay out around it. But you can nudge it from that natural
position using top, right, bottom, and left:
.nudged {
position: relative;
top: 8px; /* moves down 8px from its natural position */
left: 4px; /* moves right 4px from its natural position */
}The nudge is purely visual — the space the element would have occupied remains reserved in the flow, as if it had not moved. Other elements do not reflow.
More importantly: position: relative creates a positioned ancestor. Any
absolutely positioned descendants will anchor to this element rather than
continuing to search for one higher up the tree.
position: absolute
An absolutely positioned element is removed from normal flow entirely. Other
elements ignore it — they lay out as if it does not exist. The element is then
placed relative to the nearest ancestor with any position value other than
static, called the containing block.
.card {
position: relative; /* anchors the absolutely positioned badge */
}
.card .badge {
position: absolute;
top: 8px;
right: 8px; /* 8px from the card's top-right corner */
}If no positioned ancestor exists, the element falls back to the initial containing block — roughly the viewport for most purposes — which is almost never what you intended. This is the most common source of "my element is in a completely wrong place" confusion.
Always ask: "What is the nearest positioned ancestor of this element?" before
writing position: absolute. If you do not control what that ancestor is,
add position: relative to the intended parent explicitly. Assuming the browser
will pick the right ancestor without help is how elements end up somewhere
unexpected.
position: fixed
A fixed element is removed from flow and positioned relative to the viewport — not any ancestor element. It stays there as the page scrolls.
.sticky-header {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
}Fixed elements create a classic problem: they overlap page content. Remember to
add padding-top or margin-top to the page body equal to the fixed header's
height, so content is not hidden underneath it.
One subtlety: transform, filter, or will-change on any ancestor element
can move a fixed element off the viewport anchor and onto that ancestor instead —
the same stacking context mechanism you will meet in the next lesson.
position: sticky
Sticky positioning is a hybrid. The element behaves like position: relative until
it reaches a specified scroll threshold, then it "sticks" like position: fixed
within the boundary of its scroll container.
.section-heading {
position: sticky;
top: 0; /* sticks when it reaches the top of the viewport */
}The element stays sticky only while its parent is in view. Once the parent scrolls fully out of view, the sticky element scrolls away with it — it does not stick to the viewport indefinitely. This behavior is intentional and makes sticky perfect for section headers in long scrollable lists.
/* A practical table header that stays visible while scrolling the table body */
table thead th {
position: sticky;
top: 0;
background: white; /* prevent content bleeding through on scroll */
}Sticky positioning requires a scroll container with a defined height, and the
sticky element must be a direct child of that container (or a descendant within
it). If sticky is not working, check that no ancestor between the sticky element
and the scroll container has overflow: hidden or overflow: auto — that
collapses the scroll context and breaks the sticky behavior.
Choosing a position value
A quick guide to the decision:
| Need | Value |
|---|---|
| Default, participates in flow | static |
| Nudge an element visually without affecting flow | relative |
| Anchor for absolute children | relative |
| Overlay / badge / tooltip over a specific parent | absolute |
| Fixed header / footer visible at all times | fixed |
| Table headers, sticky section titles | sticky |
Where to go next
Positioning introduces the concept of elements layering on top of one another.
Once you have layers, you need a way to control which one draws on top. Next:
z-index and Stacking Contexts — why z-index: 9999 sometimes does nothing.
When to Use Flexbox vs Grid
Flexbox and Grid both do layout — choosing between them is a question of whether your content drives the size or your template drives the size.
z-index and Stacking Contexts
z-index controls layering — but it only works within a stacking context, which explains why z-index 9999 sometimes has no effect at all.