position is one of the most powerful — and most misunderstood — CSS properties. Choosing the wrong value results in elements floating off-screen, sticky headers that never stick, or z-index battles that never end. This guide explains every value clearly, shows when to reach for each one, and reveals the stacking-context rules that control which element sits on top.
Quick reference
| Value | In normal flow? | Positioned? | Offset relative to… |
|---|---|---|---|
static |
Yes | No | (offsets ignored) |
relative |
Yes | Yes | Its own normal-flow position |
absolute |
No | Yes | Nearest positioned ancestor |
fixed |
No | Yes | Viewport |
sticky |
Yes | Yes | Scroll container (hybrid) |
"Positioned" means the element responds to top, right, bottom, left, and participates in stacking (z-index works on it).
position: static — the default
Every element starts as static. It sits in the normal document flow, and offset properties (top, left, etc.) have no effect.
.box {
position: static; /* default — usually not written explicitly */
}
You only write static when you need to reset a positioned element back to its default.
position: relative — nudge without leaving flow
A relatively positioned element stays in the normal flow (it still occupies its original space), but you can shift it visually with top/right/bottom/left.
.nudged {
position: relative;
top: 10px; /* shifts DOWN 10px from its normal position */
left: 20px; /* shifts RIGHT 20px */
}
The space it originally occupied is preserved — neighbouring elements are not affected.
More importantly, relative creates a positioning context for any absolute children inside it. This is its most common use.
position: absolute — escape the flow, anchor to a parent
An absolutely positioned element is removed from normal flow — it no longer occupies space — and is placed relative to its nearest positioned ancestor (any element with position other than static).
.parent {
position: relative; /* establishes context */
}
.badge {
position: absolute;
top: 8px;
right: 8px;
}
If no positioned ancestor exists, the element is positioned relative to the initial containing block (roughly the <html> element), which is almost never what you want.
Common patterns with absolute
Centre inside a container:
.container {
position: relative;
width: 300px;
height: 200px;
}
.centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
Cover the entire parent:
.overlay {
position: absolute;
inset: 0; /* shorthand for top:0; right:0; bottom:0; left:0 */
}
Bottom-align a caption:
.caption {
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
position: fixed — locked to the viewport
A fixed element behaves like absolute but anchors to the viewport, not any ancestor. It does not scroll with the page.
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
}
.back-to-top {
position: fixed;
bottom: 24px;
right: 24px;
}
Watch out: A transform, filter, perspective, or will-change on any ancestor breaks fixed positioning — the element becomes fixed to that ancestor instead of the viewport. This is a common source of "my fixed element scrolls with the content" bugs.
position: sticky — scroll-triggered pinning
A sticky element behaves like relative until the scroll position reaches a threshold, then it pins like fixed — all within its scroll container and parent.
.section-header {
position: sticky;
top: 0; /* sticks when it reaches the top of the scroll container */
background: white;
z-index: 10;
}
How sticky really works
Three things must be true for sticky to work:
- Specify a threshold — at least one of
top/bottom/left/rightmust be set. - The parent must be taller than the sticky element. When the parent ends, the sticky element scrolls away with it.
- No
overflow: hiddenoroverflow: autoon any ancestor — these clip the sticky behaviour.
/* Table header that sticks while you scroll the table */
thead th {
position: sticky;
top: 0;
background: #f5f5f5;
}
z-index and stacking context
z-index controls the paint order of positioned elements. Higher values appear on top.
.modal-backdrop { z-index: 200; }
.modal { z-index: 201; }
.tooltip { z-index: 300; }
What creates a stacking context?
A stacking context is an isolated layer. z-index values inside it only compete with siblings in the same context — they cannot "escape" to the outer context.
Stacking context triggers (most common):
| Property | Value |
|---|---|
position + z-index |
any value except auto |
opacity |
< 1 |
transform |
any value except none |
filter |
any value except none |
isolation |
isolate |
will-change |
most values |
Classic z-index bug: A modal with z-index: 9999 appears behind a sidebar because the sidebar's parent has transform: translateZ(0) — creating a new stacking context. The modal is trapped inside a context with a lower order.
/* Force a new, isolated stacking context */
.modal-root {
isolation: isolate;
}
All five values — real-world layout
<div class="page">
<nav class="navbar">Fixed navbar</nav>
<section class="section">
<h2 class="sticky-heading">Sticky heading</h2>
<div class="card">
<span class="badge">New</span> <!-- absolute inside relative -->
Card content
</div>
</section>
<button class="fab">↑</button> <!-- fixed -->
</div>
.navbar {
position: fixed;
top: 0; left: 0; right: 0;
height: 60px;
z-index: 100;
}
.section { padding-top: 60px; } /* offset for fixed navbar */
.sticky-heading {
position: sticky;
top: 60px; /* below the navbar */
background: white;
z-index: 10;
}
.card { position: relative; } /* context for .badge */
.badge {
position: absolute;
top: 8px; right: 8px;
}
.fab {
position: fixed;
bottom: 24px; right: 24px;
z-index: 50;
}
6 common mistakes
1. Forgetting position: relative on the parent
You set position: absolute on a child and expect it to anchor to its parent, but no ancestor is positioned — the element flies off to the <html> corner.
Fix: Always add position: relative (or any non-static value) to the intended anchor parent.
2. z-index on a static element does nothing
z-index has no effect unless the element is positioned (relative, absolute, fixed, sticky) or is a flex/grid item.
/* Broken */
.box { position: static; z-index: 999; } /* ignored */
/* Fixed */
.box { position: relative; z-index: 999; }
3. position: sticky not sticking
The most common culprits:
- Missing threshold (
top,bottom, etc. not set) - An ancestor has
overflow: hiddenoroverflow: auto - The parent is the same height as the sticky element (nothing to stick within)
4. transform breaking fixed positioning
Adding any transform to a parent (including transform: translateZ(0) used as a GPU-compositing hack) turns it into a containing block for fixed descendants — they no longer stay fixed to the viewport.
5. Body padding/margin when using fixed at the top
A position: fixed navbar doesn't push content down. The content scrolls under it.
Fix: Add padding-top (equal to the navbar height) to <body> or the first content element.
6. Stacking context trapping z-index
A child with z-index: 9999 inside a parent with opacity: 0.99 will never appear above a sibling element that is outside that parent, regardless of its z-index value.
Fix: Use isolation: isolate deliberately, restructure the DOM, or move the element to the document root.
FAQ
Q: What's the difference between absolute and fixed?
Both are removed from flow. absolute is positioned relative to the nearest positioned ancestor; fixed is always relative to the viewport and doesn't move when the page scrolls.
Q: Can I use top/left on a static element?
No. Offset properties are ignored for static elements. Switch to relative if you just want to nudge something.
Q: Why does position: sticky work in some browsers but not others?
Old Safari required the -webkit-sticky prefix. Modern Safari (12+) supports the unprefixed version. More likely the issue is an overflow on an ancestor or a missing threshold value.
Q: What's inset in CSS?inset is a shorthand for top, right, bottom, left — just like margin is shorthand for four sides. inset: 0 is equivalent to top:0; right:0; bottom:0; left:0, and is great for position: absolute overlays.
Q: How do I stack multiple sticky elements (e.g. two sticky headers)?
Give each a different top value matching the cumulative height above:
.primary-nav { position: sticky; top: 0; height: 60px; z-index: 20; }
.secondary-nav { position: sticky; top: 60px; height: 48px; z-index: 19; }
Q: Is position: absolute bad for accessibility?
Only if it visually reorders content from the DOM order. Screen readers follow DOM order, not visual order. Use absolute positioning for decorative elements and overlays; never for reordering meaningful content (use CSS order on flex/grid instead, and even then be careful).