Badges, dropdowns, sticky headers and overlays all depend on positioning and on finding the right containing block.
position decides how top, right, bottom, left (or the shorthand inset) are applied.
static(default): normal flow; inset properties do nothing.relative: stays in flow, but is shifted visually by the offsets. Its original space is kept. Mostly used as an anchor for absolutely positioned children.absolute: taken out of flow (siblings close the gap) and positioned relative to its containing block: the nearest ancestor whose position isn't static. If none exists, the initial containing block (the viewport-sized area at the top of the page) is used.fixed: positioned relative to the viewport; it stays put when the page scrolls. (An ancestor with atransformorfilterbecomes its containing block instead, a famous gotcha.)sticky: behaves like relative until scrolling would move it past its offset, e.g.top: 0, then sticks within its parent's bounds.
The common pattern for a badge on a card: parent position: relative, child position: absolute; top: 8px; right: 8px. inset: 0 on an absolute child covers the parent completely, which is ideal for overlays.
When sticky doesn't stick, the cause is almost always an ancestor with overflow: hidden or auto (sticky then sticks inside that scroll box), or a parent no taller than the sticky element itself.
Positioned elements can overlap; z-index orders them, covered in the stacking context lesson.
Example
/* <div class="card"><span class="badge">New</span>...</div> */
.card { position: relative; } /* containing block for the badge */
.badge {
position: absolute;
top: 8px;
right: 8px;
}
.overlay { position: absolute; inset: 0; background: rgb(0 0 0 / 50%); }
.site-header {
position: sticky;
top: 0; /* sticks when it reaches the top */
background: white;
z-index: 10;
}
.chat-button { position: fixed; right: 1rem; bottom: 1rem; }Remember
- absolute is relative to the nearest positioned ancestor
- relative keeps the original space; absolute removes it
- sticky needs an offset (top: 0) and breaks inside overflow ancestors
- inset: 0 fills the containing block
Watch out for
- A sticky header that never sticks because an ancestor has overflow: hidden.