CSS The position Property
The position property tells the browser how to place an element on the page: should it flow normally with the rest of the content, or should it be pulled out and placed using coordinates? Understanding position is essential for building overlays, tooltips, sticky headers, modals, and any layout where an element needs to sit relative to something other than its natural place in the document flow.
Every element has a position value, and that value changes how four companion properties — top, right, bottom, and left (collectively called “offsets” or “inset properties”) — behave. Without a non-default position value, offsets do absolutely nothing.
Overview / How it works
CSS defines five main values for position: static, relative, absolute, fixed, and sticky. The core idea to internalize is the difference between elements that stay in normal flow and elements that are taken out of normal flow.
static is the default: the element sits exactly where the normal document flow (block and inline layout) would put it. Offsets are ignored entirely.
relative keeps the element in normal flow — its layout box still occupies its original space, and siblings are positioned as if it were never moved — but then the browser shifts the element visually using the offsets, relative to where it would otherwise have been. Nothing else reflows to fill the gap it leaves.
absolute and fixed remove the element from normal flow completely. The element no longer takes up space for layout purposes; other elements act as if it isn’t there. Instead, the browser positions it using offsets measured against a containing block. For absolute, that containing block is the nearest ancestor whose position is not static (i.e. the nearest positioned ancestor); if none exists, it’s the initial containing block (essentially the viewport/document root). For fixed, the containing block is the viewport itself (or the nearest ancestor with a transform, filter, perspective, or will-change value that creates a new containing block — a common gotcha covered below), so fixed elements stay in place even when the page scrolls.
What is a “positioned element”?
Any element whose computed position is anything other than static is called a positioned element. This matters for two reasons: it establishes a containing block for descendants using absolute, and it allows z-index to take effect (a static element with z-index set is simply ignored by the browser).
sticky is a hybrid: the element behaves like relative (stays in flow, takes up space) until the scroll position crosses a threshold defined by an offset (commonly top), at which point it “sticks” and behaves like fixed relative to its nearest scrolling ancestor — until that ancestor’s box ends, at which point it unsticks again.
Syntax
selector {
position: static | relative | absolute | fixed | sticky;
top: <length> | <percentage> | auto;
right: <length> | <percentage> | auto;
bottom: <length> | <percentage> | auto;
left: <length> | <percentage> | auto;
z-index: <integer> | auto;
}
| Value | In normal flow? | Offsets relative to |
|---|---|---|
static |
Yes | N/A (offsets ignored) |
relative |
Yes (space reserved) | Its own normal position |
absolute |
No | Nearest positioned ancestor |
fixed |
No | Viewport (usually) |
sticky |
Yes, until threshold | Nearest scrolling ancestor, after threshold |
Percentage offsets resolve against the corresponding dimension of the containing block (a top percentage uses the containing block’s height, a left percentage uses its width).
Examples
Example 1: relative for a small nudge
.badge {
position: relative;
top: -6px;
left: 4px;
background: crimson;
color: white;
padding: 2px 6px;
border-radius: 4px;
}
Result: The badge renders shifted 6px upward and 4px to the right of its normal spot in the text/inline flow. Its original space is still reserved, so neighboring inline content does not slide into the gap — the badge visually overlaps whatever sits above it.
This is the classic use of relative: a tiny visual nudge without disturbing the surrounding layout.
Example 2: absolute inside a positioned card
.card {
position: relative;
width: 280px;
padding: 16px;
border: 1px solid #ddd;
}
.card__close {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
background: #333;
color: white;
text-align: center;
line-height: 24px;
border-radius: 50%;
}
Result: A 280px card with a border renders, and a small dark circular “close” button sits pinned 8px from the card’s top edge and 8px from its right edge, floating above the card’s content regardless of how much text is inside it.
Because .card has position: relative, it becomes the containing block for .card__close. Without that, the close button’s absolute offsets would resolve against the viewport instead, likely landing in the wrong corner of the page.
Example 3: a sticky section header
.section-header {
position: sticky;
top: 0;
background: #fafafa;
padding: 8px 12px;
border-bottom: 1px solid #e0e0e0;
z-index: 10;
}
Result: As the page scrolls, each .section-header scrolls normally with its content until it reaches the top of the viewport (or its scrolling container). At that point it freezes at top: 0 and stays visible, overlapping the content scrolling underneath it, until the next sibling section pushes it back out of view.
The z-index: 10 ensures the header renders above the scrolling body text rather than behind it.
How it works step by step / Under the hood
- Step 1 — Compute the box in normal flow. The browser lays out every element as if
positionwerestatic, sizing its box according to the block/inline/flex/grid algorithm in effect. - Step 2 — Check the position value. If it’s
static, layout is done — offsets are never consulted. - Step 3 — For
relative, the browser keeps the flow-computed box exactly where it was for layout purposes (siblings, floats, and containing-block sizing all use that original position), then paints the element visually offset by the resolvedtop/right/bottom/leftvalues. If bothtopandbottomare set,topwins (mirrored: if bothleftandrightare set, in LTR writing modesleftwins). - Step 4 — For
absolute/fixed, the element is pulled out of flow entirely, and the browser walks up the ancestor chain to find the containing block (nearest positioned ancestor forabsolute; the viewport, or the nearest ancestor that establishes a new containing block viatransform/filter/will-change, forfixed). Offsets are then resolved against that containing block’s padding box, and any offset left asautolets the element’s own size/content determine that edge. - Step 5 — For
sticky, the browser computes the flow position as usual, then continuously checks scroll position against the offset threshold, clamping the element between its flow position and the sticky position as the nearest scrolling ancestor scrolls. - Step 6 — Stacking. Positioned elements with a
z-indexother thanautocreate a stacking context; among elements in the same stacking context, higherz-indexvalues paint on top.
Common Mistakes
Mistake 1: absolute positioning with no positioned ancestor
selector { position: absolute; top: 0; right: 0; }
If none of this element’s ancestors have a non-static position, the browser falls back to the initial containing block — usually the whole document. The element then jumps to the top-right of the page instead of the corner of its intended parent card or box, which surprises many beginners.
Corrected: give the intended parent position: relative so it becomes the containing block.
.parent {
position: relative;
}
.parent .corner-badge {
position: absolute;
top: 0;
right: 0;
}
Mistake 2: forgetting that z-index needs a position value
.tooltip { z-index: 999; }
On a static element (the default), z-index is completely ignored — the tooltip may still render behind other content because static elements don’t participate in explicit stacking order.
Corrected: pair z-index with a non-static position.
.tooltip {
position: absolute;
z-index: 999;
}
Mistake 3: expecting sticky to work without a scrollable ancestor or with overflow hidden
position: sticky silently stops working (behaves like relative) if any ancestor between the element and its scrolling container has overflow set to anything other than visible (e.g. hidden, auto, scroll) and constrains the sticky element’s box, or if the element’s container is exactly as tall as the element itself, leaving no room to “stick.” Always give the sticky element’s parent enough height, and audit ancestors’ overflow values when sticky doesn’t seem to trigger.
Best Practices
- Always set
position: relative(with no offsets) on the intended container before usingposition: absoluteon a child — this makes the containing block explicit and predictable. - Reach for
relativeonly for small visual nudges; use layout tools (Flexbox, Grid) for real positioning of multiple elements, sincerelative/absolutedon’t participate well in flow-based alignment. - Use
stickyinstead of JavaScript scroll listeners for headers, table-of-contents links, and table headers — it’s cheaper and jank-free. - Remember that a
transform,filter, orwill-changeon an ancestor creates a new containing block, which can accidentally breakposition: fixedchildren — check for these properties if a fixed element isn’t behaving relative to the viewport. - Keep
z-indexvalues small and intentional (e.g. a documented scale like 10, 20, 100) rather than arbitrarily large numbers, to avoid stacking wars across a codebase. - Test
stickyandfixedelements at multiple viewport sizes — content that overlaps on mobile is a very common regression.
Practice Exercises
- Build a card component with
position: relativethat contains a “NEW” ribbon badge positioned absolutely in the top-left corner, offset 12px from each edge. - Create a page layout with a navigation bar that uses
position: stickywithtop: 0so it stays visible while a long list of content scrolls beneath it. Verify it unsticks correctly at the end of its parent container. - Build a full-screen modal overlay using
position: fixedthat covers the entire viewport (top: 0; right: 0; bottom: 0; left: 0;) with a semi-transparent background, and a centered dialog box inside it positioned withabsoluterelative to the overlay.
Summary
staticis the default — offsets do nothing, and the element stays in normal flow.relativestays in flow but shifts visually using offsets, without affecting sibling layout.absoluteandfixedremove the element from flow and position it against a containing block — the nearest positioned ancestor forabsolute, the viewport (usually) forfixed.stickytoggles between flow behavior and fixed-like behavior based on scroll position and a defined offset threshold.z-indexonly affects positioned elements (non-static) and controls paint order within a stacking context.- A missing positioned ancestor, a missing
positionalongsidez-index, and overflow/height issues on sticky ancestors are the most common bugs.
