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 (or inset) 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 not position: static. For fixed, 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.

Target
I’m a normal-flow sibling. If the target is removed from flow (absolute/fixed), I move up.
Extra: the stage has position: 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 top and left set, but position: static will ignore them.

Badge

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 gets position: 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;
}
  
R

With 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 position set to relative, absolute, fixed, or sticky.

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 position: relative. The “POP” box is position: absolute, so it can pin itself inside the card.

Sibling content still flows normally
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 with transform: translate(-50%, -50%). Great when you don’t know the element’s size.
  • Margin auto + inset method: set inset: 0, give it a size, then margin: 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;
}
  

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 box

Scroll 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 top so it won’t stick. Try snippet 2: it sticks. Try snippet 3: overflow: hidden can stop sticky from behaving as you expect. Try snippet 4: overflow: auto makes the scroller the scroll container (sticky sticks within it).

Sticky header

Scroll 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 content
More content
Even more content

The modal is position: fixed, so it’s centered in the viewport.

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 position set to relative, absolute, fixed, or sticky (not static).
  • Flex/Grid items can also overlap and layer in ways that make z-index matter, 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-index on position: static won’t do what you expect.

A
B

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 (position not static) with a z-index value (even 0).
  • An element with opacity less than 1.
  • An element with transform (like transform: translateZ(0) or transform: translate(...)).
  • An element with filter (like filter: 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:

  1. Is the element positioned? If it’s position: static, try position: relative.
  2. Are you fighting stacking contexts? A parent might be creating a stacking context and trapping your element.
  3. Is another element creating a new context above you? For example, a sibling with opacity: 0.9 or transform.
  4. 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: 999 on 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-index seems ignored, ensure the element is not position: static.
  • If z-index works “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 static when you want normal flow (and remember: offsets won’t apply).
  • Use relative for small nudges and to create an anchor for absolute children.
  • Use absolute for overlays, badges, and “pin this inside that”.
  • Use sticky for headers/sidebars that should stick within a scroll container.
  • Use fixed for UI that should stay glued to the viewport.