Five values
| Value | In flow? | Offsets are relative to |
|---|---|---|
static | Yes | Nothing — top/left are ignored. The default. |
relative | Yes — the space is kept | Where it would have been. |
absolute | No | The nearest positioned ancestor. |
fixed | No | The viewport. |
sticky | Yes | Its scroll container, between two thresholds. |
relative
position: relative does two useful things, and people usually want the second one: it lets you nudge an element without disturbing anything around it, and it makes the element a containing block for absolutely positioned children.
.card {
position: relative; /* so the badge can be positioned against THIS */
}
.card .badge {
position: absolute;
top: 8px;
right: 8px;
}
absolute
An absolutely positioned element is removed from flow: nothing reserves space for it, and it cannot push anything. It is placed against its containing block — the padding box of the nearest ancestor that is itself positioned (not static).
If no ancestor is positioned, it goes all the way up to the initial containing block, which is the size of the viewport. That is why a badge sometimes ends up in the top corner of the page instead of the card.
.overlay {
position: absolute;
inset: 0; /* top/right/bottom/left: 0 */
}
.overlay-inset {
position: absolute;
inset: 1rem 2rem; /* block, inline */
}
Setting both left: 0 and right: 0 stretches it to the container's width — you do not need width: 100%, and it keeps working when the container has padding.
fixed
Fixed to the viewport: it does not move when the page scrolls. The usual uses are a header, a cookie banner and a floating action button.
The exception that surprises everyone: a transform, filter, backdrop-filter, perspective, contain or will-change on any ancestor makes that ancestor the containing block instead — and the fixed element scrolls with it. A transform: translateZ(0) added for performance is the usual culprit.
sticky
sticky is in flow until it reaches a threshold, then it stops there while its parent scrolls past. It is the one that needs the most care to get right.
- It must have a threshold: at least one of
top,bottom,leftorright. - It sticks within its parent, and it stops when the parent scrolls out. A sticky sidebar inside a short container stops almost immediately — that is usually the bug.
- Any ancestor with
overflow: hidden,autoorscrollbecomes the scroll container, and sticky then behaves relative to that. Anoverflow: hiddenseveral levels up is the commonest reason sticky "does not work". - Add
scroll-margin-topto anchor targets so a sticky header does not cover them.
thead th {
position: sticky;
top: 0;
background: #fff; /* it is transparent by default, and content shows through */
}
z-index and stacking contexts
z-index only applies to positioned elements (and to flex/grid items). It orders siblings within the same stacking context — it cannot lift an element out of its parent's layer.
That is why z-index: 9999 sometimes does nothing: the element is inside a stacking context that sits below the thing you are trying to cover.
- A stacking context is created by: the root element;
positionother than static with az-indexother than auto;opacityless than 1;transform,filter,backdrop-filter,perspective,mix-blend-mode;isolation: isolate;will-changeon any of those; acontain: paint. isolation: isolatecreates one deliberately, with no other side effects — it is the clean way to stop a component's z-indexes leaking into the page.
.component {
isolation: isolate; /* z-indexes inside are now sealed in */
}
.modal {
position: fixed;
inset: 0;
z-index: 100; /* only has to beat its siblings */
}
The top layer
<dialog> opened with showModal(), and an element with popover, are rendered in the top layer — above every stacking context on the page, whatever the z-indexes say. It is the only way to be certain something is on top, and it is another reason to prefer the native elements.
Centring an absolute element
/* known size not required */
.centre {
position: absolute;
inset: 0;
margin: auto;
width: fit-content;
height: fit-content;
}
/* the classic */
.centre-t {
position: absolute;
top: 50%;
left: 50%;
translate: -50% -50%; /* percentages of its OWN size */
}
When not to use it
Absolute positioning takes the element out of flow, so nothing around it can respond to its size. If the thing you are placing is part of the layout — a sidebar, a column, a card in a deck — grid or flex will do it better and stay responsive.
Keep absolute for genuine overlays: a badge on a card, a caption over an image, a dropdown, a tooltip.