CSS Positioning
CSS positioning controls how an element is placed relative to normal flow, its own original position, a containing block, a viewport or a scrolling area. The position property supports five commonly used values: static, relative, absolute, fixed and sticky.
Normal flow
├── static -> Normal position
├── relative -> Normal position, then visually offset
└── sticky -> Normal flow until a scrolling threshold
Out of normal flow
├── absolute -> Positioned from a containing block
└── fixed -> Usually positioned from the viewport
One-line idea: Choose a positioning mode by deciding whether the element should keep its place in normal flow and which box should control its offsets.
Core Concepts
Start with Normal Flow
Before applying positioning, the browser places elements in normal flow according to their display type and layout context.
<h1>Products</h1>
<p>Browse the latest items.</p>
<button>View cart</button>
Block elements usually stack in the block direction, while inline content flows within lines. Flexbox and Grid also define structured layout flows.
Positioning should solve a placement need, not replace Flexbox or Grid for an entire page layout.
Inset Properties
Positioned elements can use physical inset properties:
.box {
top: 1rem;
right: 2rem;
bottom: auto;
left: 0;
}
The inset shorthand can set all four sides:
.overlay {
position: absolute;
inset: 0;
}
Logical inset properties adapt to writing direction:
.badge {
position: absolute;
inset-block-start: 0.5rem;
inset-inline-end: 0.5rem;
}
Properties such as inset-inline-end are often preferable to hard-coding right in components that support both left-to-right and right-to-left languages.
Position Values
position: static
static is the default. The element participates in normal flow, and the inset properties do not reposition it.
.article {
position: static;
}
Before: Heading -> Article -> Footer
After: Heading -> Article -> Footer
Use it for ordinary content that does not need positioned offsets.
position: relative
A relatively positioned element keeps its original space in normal flow, then is visually offset from that position.
.label {
position: relative;
inset-block-start: 0.25rem;
}
Layout reserves the original space
↓
Element is painted at an offset
↓
Nearby elements do not move into the old space
Common uses include:
- small visual offsets
- creating a containing block for positioned descendants
- participating in stacking with
z-index
Avoid using relative offsets to repair a layout that should instead use spacing, alignment, Flexbox or Grid.
position: absolute
An absolutely positioned element is removed from normal flow. Its offsets are calculated from its containing block.
<article class="product-card">
<img src="shoe.webp" alt="Blue running shoe" />
<span class="badge">New</span>
</article>
.product-card {
position: relative;
}
.badge {
position: absolute;
inset-block-start: 0.75rem;
inset-inline-end: 0.75rem;
}
The positioned card establishes the containing block for the badge. The badge occupies no normal-flow space, so other content behaves as though it is not there.
Common uses include:
- notification badges
- controls layered over media
- decorative elements
- menus and popovers when CSS anchoring or top-layer APIs are not being used
position: fixed
A fixed element is removed from normal flow and is usually positioned relative to the viewport:
.back-to-top {
position: fixed;
inset-inline-end: 1rem;
inset-block-end: 1rem;
}
It normally remains at the same viewport position while the document scrolls.
Common uses include:
- floating action buttons
- persistent toolbars
- back-to-top controls
- viewport-level overlays
Fixed positioning has an important exception: properties such as transform, perspective or filter on an ancestor can establish the fixed element's containing block. In that case, it may behave as fixed to that ancestor rather than to the viewport.
position: sticky
A sticky element participates in normal flow. It behaves like a relatively positioned element until scrolling would move it beyond a specified inset threshold; it is then constrained within its scrolling area and containing block.
.section-heading {
position: sticky;
inset-block-start: 0;
background: white;
}
Element scrolls in normal flow
↓
Reaches the top inset threshold
↓
Stays against that edge while allowed
↓
Stops at its containing boundary
At least one inset on the sticky axis must be non-auto. Without top, bottom or the corresponding logical inset, sticky positioning behaves like relative positioning on that axis.
Common uses include:
- section headings
- table headers
- filter or navigation sidebars
- controls that should remain visible within a panel
Positioning Comparison
| Value | In normal flow? | Insets apply? | Offset reference | Typical use |
|---|---|---|---|---|
static | Yes | No | Normal layout | Standard content |
relative | Yes; original space remains | Yes | Its normal position | Anchor or small offset |
absolute | No | Yes | Containing block | Badge, overlay or precise layer |
fixed | No | Yes | Usually viewport | Persistent viewport control |
sticky | Yes | Yes | Scroll container and containing block | Sticky header or sidebar |
Quick Mental Model
static = Keep me in normal flow
relative = Keep my space, then offset me
absolute = Remove my space and place me in a containing block
fixed = Keep me at a viewport position, usually
sticky = Keep my space, then constrain me during scrolling
Containing Blocks
Why the Containing Block Matters
Offsets and percentage sizes are resolved against a reference box called the containing block. It is not always simply the direct parent.
For a typical absolutely positioned element, the containing block is established by the nearest ancestor whose position is not static:
<section class="panel">
<div class="content">
<button class="close">Close</button>
</div>
</section>
.panel {
position: relative;
}
.close {
position: absolute;
top: 0;
right: 0;
}
Even though .content is the direct parent, .panel is the positioned ancestor and therefore the typical containing block.
Other properties, including transforms and containment, can also establish containing blocks. Inspect the ancestor chain when an element positions itself relative to an unexpected box.
Stretching an Absolutely Positioned Element
When width is auto, setting both horizontal insets can stretch a non-replaced absolutely positioned element across the available space:
.banner {
position: absolute;
left: 1rem;
right: 1rem;
}
The same idea applies vertically with top and bottom when height remains auto.
Sticky Positioning in Practice
Sticky Uses the Nearest Scrolling Mechanism
A sticky element sticks relative to its nearest relevant ancestor with a scrolling mechanism, commonly created by overflow: auto, scroll, hidden or overlay.
.table-wrapper {
max-block-size: 24rem;
overflow: auto;
}
thead {
position: sticky;
top: 0;
background: white;
}
Here the table header sticks inside .table-wrapper, not necessarily against the browser viewport.
Why Sticky Appears Not to Work
Check these conditions:
- Is an inset such as
top: 0set on the correct axis? - Which ancestor creates the scrolling mechanism?
- Is there enough scrollable distance for the element to move?
- Does the containing block end before the desired sticky range?
- Is the sticky element as tall as, or stretched to the height of, its container?
- Is another element painting over it because of stacking order?
Do not automatically remove overflow: hidden; it may be required for clipping. First identify which ancestor should own scrolling.
Stacking and z-index
z-index Is Contextual
When positioned elements overlap, z-index helps determine their stacking level within the current stacking context.
.dialog-backdrop {
position: fixed;
inset: 0;
z-index: 10;
}
.dialog {
position: fixed;
z-index: 20;
}
A higher number is not globally above every lower number. Descendants are constrained by the stacking context created by their ancestors.
Root stacking context
├── Header: z-index 10
└── Main: z-index 1
└── Tooltip: z-index 9999
The tooltip can still appear below the header because the entire Main stacking context is below the header.
What Creates a Stacking Context?
Common causes include:
- the root element
- positioned elements with a non-
autoz-index position: fixedorstickyopacitybelow1- transforms and filters
isolation: isolate- certain containment and container-query values
- elements placed in the browser's top layer
When z-index seems ineffective, inspect ancestor stacking contexts instead of continually increasing the number.
Practical Patterns
Badge Inside a Card
.avatar {
position: relative;
inline-size: 4rem;
block-size: 4rem;
}
.status-dot {
position: absolute;
inset-inline-end: 0;
inset-block-end: 0;
inline-size: 0.875rem;
block-size: 0.875rem;
border: 2px solid white;
border-radius: 50%;
background: green;
}
The avatar stays in flow, while the status indicator is layered inside its bounds.
Fixed Header Without Covered Content
A fixed header does not reserve layout space. Add intentional spacing so it does not cover the page:
:root {
--header-height: 4rem;
}
.site-header {
position: fixed;
inset: 0 0 auto;
min-block-size: var(--header-height);
}
main {
padding-block-start: var(--header-height);
}
If the header does not need to remain detached from a container, sticky may require less manual compensation.
Responsive and Accessible Positioning
- Ensure fixed or sticky UI does not cover focused controls or headings.
- Test browser zoom, large text and narrow viewports.
- Preserve logical reading and keyboard order in the DOM.
- Do not use visual positioning to create a different semantic order.
- Respect safe-area insets for controls near mobile screen edges.
- Keep dismiss buttons reachable when overlays are zoomed.
.floating-action {
position: fixed;
inset-inline-end: max(1rem, env(safe-area-inset-right));
inset-block-end: max(1rem, env(safe-area-inset-bottom));
}
Performance Considerations
Fixed and sticky elements can require frequent repainting or compositing during scroll, especially when they are large or use expensive visual effects.
- Measure scrolling in browser performance tools.
- Avoid large shadows or filters when profiling shows costly paint.
- Do not add
will-changeto every positioned element. - Keep overlay and stacking structures understandable.
- Prefer transforms for motion when they meet the layout requirement.
Common Mistakes
| Mistake | Better approach |
|---|---|
| Using absolute positioning for the whole page layout | Use normal flow, Flexbox or Grid for structural layout |
| Saying absolute always uses its parent | Identify the actual containing block |
Forgetting position: relative on an intended anchor | Establish the containing block deliberately |
| Assuming fixed always uses the viewport | Check ancestors for transforms, filters, perspective or containment |
| Expecting sticky to work without an inset | Set a non-auto inset on the sticky axis |
| Ignoring ancestor overflow when debugging sticky | Find the nearest scrolling mechanism |
| Treating sticky as removed from flow | Remember that it keeps its normal-flow space |
Raising z-index indefinitely | Inspect the stacking-context hierarchy |
| Reordering content visually with offsets | Keep DOM order aligned with reading and focus order |
| Letting fixed UI cover content at zoom | Test narrow, zoomed and large-text layouts |
Official References
- MDN:
position - MDN: CSS Positioning
- MDN: Layout and the Containing Block
- MDN: Stacking Context
- MDN:
z-index
Summary
CSS positioning controls whether an element participates in normal flow and which containing block, viewport or scroll boundary determines its final placement.
Should the element keep space in normal flow?
├── Yes
│ ├── No offset needed -> static
│ ├── Offset from itself -> relative
│ └── Constrain during scroll -> sticky
└── No
├── Position inside a containing block -> absolute
└── Position near the viewport -> fixed, usually
Then inspect containing blocks, insets and stacking contexts.