CSS Position: the mental model
The CSS position property controls how an element is laid out and how it responds to
“offset” properties like top, right, bottom, and left (and their
logical cousins: inset-block-start, inset-inline-start, etc.).
Think of positioning as choosing a “coordinate system” for an element:
- static: normal document flow (offsets do nothing).
- relative: normal flow, but you can nudge the element visually.
- absolute: removed from flow and positioned against a containing block.
- sticky: behaves like relative until it hits a threshold, then sticks within a scroll container.
- fixed: removed from flow and positioned relative to the viewport.
Two important supporting actors:
-
Offsets:
top,right,bottom,left(orinset) only “work” for non-static positioned elements. -
Containing block: the reference box used for positioning. For
absolute, it’s usually the nearest ancestor that is notposition: static. Forfixed, it’s usually the viewport.
.demo {
position: static;
}
.demo {
position: relative;
top: 14px;
left: 18px;
}
.demo {
position: absolute;
top: 14px;
left: 18px;
}
.demo {
position: fixed;
top: 14px;
left: 18px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.note {
border: 2px dashed #111;
border-radius: 14px;
padding: 12px;
background: #fff;
}
.stage {
border: 3px solid #111;
border-radius: 18px;
background: #f6f6f6;
padding: 14px;
min-height: 220px;
position: relative;
overflow: hidden;
}
.row {
display: grid;
gap: 10px;
}
.demo {
width: 160px;
height: 90px;
border: 3px solid #111;
border-radius: 14px;
background: #fff;
display: grid;
place-items: center;
font-weight: 700;
box-shadow: 0 10px 0 #111;
}
.sibling {
width: 100%;
border: 2px solid #111;
border-radius: 14px;
padding: 10px;
background: #fff;
}
.hint {
margin: 0;
opacity: 0.8;
}
Click the snippets. Watch what happens to the blue-ish box and to the “sibling” box below it.
TargetI’m a normal-flow sibling. If the target is removed from flow (absolute/fixed), I move up.Extra: the stage hasposition: relative, so absolute positioning has a reference.
CSS position property options
Here are the common values you’ll use (and what they mean in one sentence):
-
static: default, in normal flow; offsets don’t apply. -
relative: stays in flow; offsets move it visually. -
absolute: removed from flow; positioned relative to a containing block. -
sticky: “sticks” after crossing a threshold inside a scroll container. -
fixed: removed from flow; positioned relative to the viewport.
There’s also position: inherit (copy from parent) and position: initial (reset to default),
but those are less exciting.
CSS position static
position: static is the default. The element participates in normal document flow and
ignores top, left, etc.
If you ever find yourself typing position: static;, it’s usually because you’re undoing a more specific
rule (like “reset this component back to normal”).
.badge {
position: static;
top: -20px;
left: 40px;
}
.badge {
position: relative;
top: -20px;
left: 40px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.card {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 14px;
position: relative;
}
.title {
margin: 0 0 8px;
}
.badge {
display: inline-block;
padding: 6px 10px;
border: 2px solid #111;
border-radius: 999px;
background: #f6f6f6;
font-weight: 700;
}
Static ignores offsets
This badge has
Badgetopandleftset, butposition: staticwill ignore them.
Learn more in the CSS Position Static Interactive Tutorial.
CSS position relative
position: relative keeps the element in normal flow (so it still takes up space), but
lets you visually shift it with offsets.
Two big uses for relative:
- Small nudges: “Move this icon 2px down because my eyes said so.”
-
Create a containing block for absolutely positioned children: parent gets
position: relative, child getsposition: absolute.
.pin {
position: relative;
top: 12px;
left: 10px;
}
.pin {
position: relative;
top: -12px;
left: -10px;
}
.pin {
position: relative;
top: 0;
left: 0;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.stage {
border: 3px solid #111;
border-radius: 18px;
background: #f6f6f6;
padding: 14px;
}
.line {
border: 2px dashed #111;
border-radius: 14px;
padding: 14px;
background: #fff;
display: flex;
gap: 10px;
align-items: center;
}
.pin {
width: 56px;
height: 56px;
border: 3px solid #111;
border-radius: 16px;
background: #fff;
display: grid;
place-items: center;
font-weight: 800;
box-shadow: 0 10px 0 #111;
}
.text {
margin: 0;
}
RWith
position: relative, the element still reserves space in the layout, even when it moves.
Learn more in the CSS Position Relative Interactive Tutorial.
CSS position absolute
position: absolute removes the element from normal flow and positions it using offsets.
That means:
- It no longer takes up space (siblings behave like it isn’t there).
-
It positions itself relative to a containing block, typically the nearest ancestor with
positionset torelative,absolute,fixed, orsticky.
The most common “why is my absolute element flying across the page?” issue is simply:
you forgot to set position: relative on the parent.
.pop {
position: absolute;
top: 12px;
right: 12px;
}
.pop {
position: absolute;
bottom: 12px;
left: 12px;
}
.pop {
position: absolute;
inset: 12px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.card {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 16px;
position: relative;
min-height: 220px;
overflow: hidden;
}
.card h4 {
margin: 0 0 8px;
}
.card p {
margin: 0;
max-width: 56ch;
}
.pop {
border: 3px solid #111;
border-radius: 16px;
background: #f6f6f6;
padding: 10px 12px;
font-weight: 800;
box-shadow: 0 10px 0 #111;
}
.badge {
display: inline-block;
margin-top: 10px;
padding: 6px 10px;
border: 2px dashed #111;
border-radius: 999px;
background: #fff;
font-weight: 700;
}
Absolute positioning anchors to a positioned parent
This card has
Sibling content still flows normallyposition: relative. The “POP” box isposition: absolute, so it can pin itself inside the card.POP
The inset property is a shorthand for top, right, bottom, and
left.
CSS position absolute center
Centering an absolutely positioned element is a classic CSS rite of passage. Here are two reliable methods:
-
Transform method:
top: 50%,left: 50%, then pull it back withtransform: translate(-50%, -50%). Great when you don’t know the element’s size. -
Margin auto + inset method: set
inset: 0, give it a size, thenmargin: auto. Great when you do know the size (or can set a max size).
.modal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.modal {
position: absolute;
inset: 0;
margin: auto;
width: min(360px, 92%);
height: 160px;
}
.modal {
position: absolute;
inset: 24px;
margin: auto;
width: min(410px, 92%);
height: 190px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.stage {
border: 3px solid #111;
border-radius: 18px;
background: #f6f6f6;
padding: 14px;
min-height: 280px;
position: relative;
overflow: hidden;
}
.bg {
position: absolute;
inset: 0;
background: url("https://picsum.photos/1100/500") center / cover no-repeat;
opacity: 0.28;
}
.modal {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 14px;
display: grid;
gap: 8px;
align-content: center;
text-align: center;
}
.modal h4 {
margin: 0;
}
.modal p {
margin: 0;
opacity: 0.85;
}
.pill {
justify-self: center;
border: 2px solid #111;
border-radius: 999px;
padding: 6px 10px;
background: #f6f6f6;
font-weight: 800;
}
Centered modal
Try both centering methods in the snippets.
Absolute Center
Learn more in the CSS Position Absolute Interactive Tutorial.
CSS position sticky
position: sticky is like a polite mix of relative and fixed:
- It acts like a normal element until you scroll far enough…
-
…then it “sticks” when its offset threshold is reached (like
top: 0). - It only sticks within its scroll container (the nearest ancestor that scrolls).
Sticky needs at least one offset to know when to stick (commonly top).
.sticky {
position: sticky;
top: 0;
z-index: 1;
}
.sticky {
position: sticky;
top: 24px;
z-index: 1;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.scroller {
border: 3px solid #111;
border-radius: 18px;
background: #f6f6f6;
padding: 14px;
height: 320px;
overflow: auto;
}
.sticky {
border: 3px solid #111;
border-radius: 16px;
background: #fff;
box-shadow: 0 10px 0 #111;
padding: 12px;
font-weight: 800;
}
.item {
border: 2px dashed #111;
border-radius: 14px;
padding: 12px;
background: #fff;
margin-top: 12px;
}
.item p {
margin: 0;
opacity: 0.85;
}
I stick inside this scrolling boxScroll me…
Keep going…
Sticky should stay visible…
…until the container ends.
Almost there…
Sticky is not “global fixed”, it’s container-bound.
CSS position sticky not working
When sticky refuses to stick, it’s usually one of these issues.
1) Sticky needs an offset
If you don’t set top (or bottom, etc.), sticky doesn’t know when to start sticking.
2) A parent has overflow that changes the scroll container
Sticky sticks within the nearest scrolling ancestor. If a parent has overflow: hidden or
overflow: auto, that parent can become the scroll container (or clip the sticky behavior).
3) There isn’t enough scroll space
If the container doesn’t scroll, you won’t see sticky do anything. Sticky is a scroll-based effect.
4) Layout constraints can break expectations
Sticky can behave unexpectedly inside certain layouts (like when you’re scrolling a different element than you think). The fix is usually: make sure the element you scroll is the one you intend sticky to stick within.
.sticky {
position: sticky;
}
.sticky {
position: sticky;
top: 10px;
}
.scroller {
overflow: hidden;
}
.sticky {
position: sticky;
top: 10px;
}
.scroller {
overflow: auto;
}
.sticky {
position: sticky;
top: 10px;
z-index: 1;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.note {
border: 2px dashed #111;
border-radius: 14px;
padding: 12px;
background: #fff;
}
.scroller {
border: 3px solid #111;
border-radius: 18px;
background: #f6f6f6;
padding: 14px;
height: 280px;
}
.inner {
height: 520px;
display: grid;
gap: 12px;
}
.sticky {
border: 3px solid #111;
border-radius: 16px;
background: #fff;
box-shadow: 0 10px 0 #111;
padding: 12px;
font-weight: 900;
}
.filler {
border: 2px dashed #111;
border-radius: 14px;
padding: 12px;
background: #fff;
}
.filler p {
margin: 0;
opacity: 0.85;
}
Try snippet 1: sticky has no
topso it won’t stick. Try snippet 2: it sticks. Try snippet 3:overflow: hiddencan stop sticky from behaving as you expect. Try snippet 4:overflow: automakes the scroller the scroll container (sticky sticks within it).Sticky headerScroll content
More scroll content
Even more scroll content
Sticky only “sticks” while scrolling happens
Learn more in the CSS Position Sticky Interactive Tutorial.
CSS position fixed
position: fixed removes the element from normal flow and anchors it relative to the
viewport. It stays put even when the page scrolls.
Typical uses:
- Floating action buttons (FABs)
- Sticky toolbars that should always be visible
- “Back to top” buttons
One gotcha: fixed elements can overlap content. Often you’ll add padding/margin to the page to make space, or you’ll place fixed UI in a corner so it stays out of the way.
.fab {
position: fixed;
right: 18px;
bottom: 18px;
}
.fab {
position: fixed;
left: 18px;
top: 18px;
}
.fab {
position: fixed;
inset: auto 18px 18px auto;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.panel {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 14px;
}
.panel p {
margin: 0;
opacity: 0.85;
}
.fab {
border: 3px solid #111;
border-radius: 999px;
background: #f6f6f6;
box-shadow: 0 10px 0 #111;
padding: 10px 14px;
font-weight: 900;
z-index: 10;
}
The “FAB” is fixed to the viewport. Switch snippet and notice it stays pinned, but changes position.
Learn more in the CSS Position Fixed Interactive Tutorial.
CSS position center
There is no position: center value (if only). When people say “position center”, they typically mean:
- Center an element inside a container
- Center an overlay/modal on the page
- Center something in the viewport
Here are the most common “centering recipes” using positioning.
Center with absolute positioning
Use this when the centered element should be anchored inside a specific container (like a card, hero, or image frame).
.frame {
position: relative;
}
.center-me {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.frame {
position: relative;
}
.center-me {
position: absolute;
inset: 0;
margin: auto;
width: 220px;
height: 120px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.frame {
border: 3px solid #111;
border-radius: 18px;
background: url("https://picsum.photos/1000/500") center / cover no-repeat;
min-height: 280px;
overflow: hidden;
}
.overlay {
position: absolute;
inset: 0;
background: #fff;
opacity: 0.25;
}
.center-me {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 14px;
text-align: center;
display: grid;
gap: 8px;
align-content: center;
}
.center-me h4 {
margin: 0;
}
.center-me p {
margin: 0;
opacity: 0.85;
}
Centered overlay
Two absolute centering methods.
Center in the viewport with fixed positioning
Use this for global modals that should remain centered even if the page scrolls.
.modal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.backdrop {
position: absolute;
}
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.backdrop {
position: fixed;
}
.modal {
position: fixed;
inset: 0;
margin: auto;
width: min(410px, 92%);
height: 180px;
}
.backdrop {
position: fixed;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.page {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 14px;
display: grid;
gap: 10px;
}
.block {
border: 2px dashed #111;
border-radius: 14px;
padding: 12px;
height: 120px;
background: #f6f6f6;
}
.backdrop {
inset: 0;
background: #111;
opacity: 0.12;
pointer-events: none;
}
.modal {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 14px;
display: grid;
gap: 8px;
align-content: center;
text-align: center;
z-index: 20;
}
.modal h4 {
margin: 0;
}
.modal p {
margin: 0;
opacity: 0.85;
}
Pretend page contentMore contentEven more contentThe modal is
position: fixed, so it’s centered in the viewport.Fixed centered modal
Still centered even if you scroll the page, with snippet 2 and 3
Learn more about centering with CSS in the Centering With CSS Interactive Tutorial.
CSS z-index and stacking context
If position is how elements get their “coordinates”, then z-index is how
they decide who stands in front of who.
It controls the stacking order along the imaginary Z axis (toward/away from the viewer).
z-index basics
z-index only applies to elements that participate in stacking in a meaningful way, most commonly:
-
Elements with
positionset torelative,absolute,fixed, orsticky(notstatic). -
Flex/Grid items can also overlap and layer in ways that make
z-indexmatter, but the “positioned element” rule is the one you’ll hit first as a beginner.
Higher z-index values appear on top of lower ones, but only within the same stacking context.
That last part is the reason “my z-index: 999999 doesn’t work” is a universal developer experience.
.a {
position: relative;
z-index: 1;
}
.b {
position: relative;
z-index: 2;
}
.a {
position: relative;
z-index: 3;
}
.b {
position: relative;
z-index: 1;
}
.a {
position: static;
z-index: 999;
}
.b {
position: relative;
z-index: 1;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.stage {
border: 3px solid #111;
border-radius: 18px;
background: #f6f6f6;
padding: 14px;
min-height: 240px;
position: relative;
}
.tile {
width: 220px;
height: 140px;
border: 3px solid #111;
border-radius: 18px;
box-shadow: 0 12px 0 #111;
display: grid;
place-items: center;
font-weight: 900;
}
.a {
background: #fff;
position: absolute;
top: 24px;
left: 24px;
}
.b {
background: #fff;
position: absolute;
top: -30px;
left: 130px;
}
.note {
margin: 0 0 10px;
opacity: 0.85;
}
The boxes overlap. Switch snippets to change who’s on top. Snippet 3 shows a classic gotcha:
z-indexonposition: staticwon’t do what you expect.AB
In snippet 3, the z-index on the static element doesn’t work because you can't apply
z-index to elements with position: static.
What is a stacking context?
A stacking context is like a mini “layer universe”. Inside that universe, children stack relative to each other. But that universe itself is then stacked as a single unit in the parent universe.
The key consequence:
A child can’t escape its parent’s stacking context.
So a child with z-index: 9999 can still be stuck underneath a sibling from a different stacking context
if its parent is behind.
Common ways stacking contexts are created
There are quite a few triggers in CSS, but you’ll most often meet these:
-
A positioned element (
positionnotstatic) with az-indexvalue (even0). -
An element with
opacityless than1. -
An element with
transform(liketransform: translateZ(0)ortransform: translate(...)). -
An element with
filter(likefilter: blur(0)). -
An element with
isolation: isolate.
These can be sneaky: you add a harmless-looking opacity: 0.99 or a transform for animation,
and suddenly your overlays start ignoring your z-index numbers.
Why your z-index “doesn’t work”
If your z-index isn’t doing anything, check these in order:
-
Is the element positioned? If it’s
position: static, tryposition: relative. - Are you fighting stacking contexts? A parent might be creating a stacking context and trapping your element.
-
Is another element creating a new context above you? For example, a sibling with
opacity: 0.9ortransform. - Are you using “random huge z-index values”? You can, but it’s better to make your stacking intentional (more on that below).
.panel {
position: relative;
z-index: 1;
}
.badge {
position: absolute;
z-index: 999;
}
.panel {
position: relative;
z-index: 1;
opacity: 0.99;
}
.badge {
position: absolute;
z-index: 999;
}
.panel {
position: relative;
z-index: 1;
transform: translateY(0);
}
.badge {
position: absolute;
z-index: 999;
}
.panel {
position: relative;
z-index: 1;
}
.banner {
position: relative;
z-index: 2;
top:40px;
}
.badge {
position: absolute;
z-index: 999;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.stage {
border: 3px solid #111;
border-radius: 18px;
background: #f6f6f6;
padding: 14px;
position: relative;
min-height: 260px;
overflow: hidden;
}
.panel {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 16px;
position: relative;
margin-top: 40px;
}
.panel h4 {
margin: 0 0 8px;
}
.panel p {
margin: 0;
max-width: 58ch;
opacity: 0.85;
}
.badge {
border: 3px solid #111;
border-radius: 999px;
background: #f6f6f6;
box-shadow: 0 10px 0 #111;
padding: 8px 12px;
font-weight: 900;
top: -18px;
right: 16px;
}
.banner {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 12px;
font-weight: 900;
position: absolute;
top: 10px;
left: 10px;
width: calc(100% - 20px);
}
.note {
border: 2px dashed #111;
border-radius: 14px;
padding: 12px;
background: #fff;
margin: 0;
opacity: 0.85;
}
Snippet 1: normal layering. Snippet 4: the banner’s stacking context is above the panel, so even
z-index: 999on the badge can’t jump above it because the badge is trapped inside the panel’s context.Panel content
The badge tries to sit above everything. Sometimes it can. Sometimes it can’t. That’s stacking contexts doing their thing.
Badge
A simple z-index strategy that stays sane
Instead of sprinkling random numbers like confetti, pick a tiny scale and stick to it. For example:
- 0: normal content
- 10: sticky headers
- 20: dropdowns / popovers
- 30: modals
- 40: toasts / critical overlays
The real trick is not the numbers, it’s controlling where stacking contexts are created. If you create too many (via
z-index, opacity, transform),
layering becomes a detective story.
Quick checklist
-
If
z-indexseems ignored, ensure the element is notposition: static. -
If
z-indexworks “sometimes”, look for stacking context creators on ancestors (z-index,opacity,transform). -
If you need something to overlay the whole page, put it near the end of the DOM (or in a top-level container) and
use
position: fixed.
Learn more in the CSS Z-Index Interactive Tutorial, and in the CSS Stacking Context Interactive Tutorial.
Wrap-up: choosing the right position value
-
Use
staticwhen you want normal flow (and remember: offsets won’t apply). -
Use
relativefor small nudges and to create an anchor for absolute children. -
Use
absolutefor overlays, badges, and “pin this inside that”. -
Use
stickyfor headers/sidebars that should stick within a scroll container. -
Use
fixedfor UI that should stay glued to the viewport.
