I've lost count of how many times I've watched someone slap position: absolute on an element, refresh the page, and then stare at the screen in confusion as the element teleports to some random corner. CSS positioning is one of those things that seems simple until it isn't — and by then you've wasted 45 minutes fighting with z-index: 99999.
Let's fix that. I'm going to walk through every CSS position value with actual examples, explain when to use each one, and — most importantly — demystify the stacking context so you never have to guess about z-index again.
Position: Static — The Default You Forget About
Every element starts with position: static. It just means "put this where it normally goes in the document flow." You almost never write it explicitly, but it's worth understanding because every other position value is defined relative to this default behavior.
/* This does literally nothing different */
.element {
position: static;
top: 20px; /* ← ignored completely */
left: 30px; /* ← also ignored */
}
Here's the thing people miss: top, right, bottom, left, and z-index have zero effect on static elements. If you're setting those properties and nothing's happening, check if you forgot to set position to something other than static.
Position: Relative — The Gentle Nudge
Relative positioning keeps the element in the normal flow but lets you nudge it from its original position. The key insight? The space the element originally occupied is preserved. Other elements don't collapse into that space.
.card {
position: relative;
top: 10px;
left: 20px;
}
/* The card moves 10px down and 20px right
from where it WOULD have been.
But its original space remains "reserved." */
I use relative positioning for two main scenarios. First, small visual tweaks — maybe an icon needs to be 2px higher to look visually centered. Second (and more commonly), as a positioning anchor for absolutely-positioned children. More on that in a sec.
Quick Example: Badge on a Card
.card {
position: relative; /* This is the anchor */
width: 300px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.badge {
position: absolute; /* Positioned relative to .card */
top: -8px;
right: -8px;
background: #ef4444;
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 0.75rem;
font-weight: 700;
}
That's the classic pattern — parent gets position: relative, child gets position: absolute. You'll use this combo hundreds of times in your career.
Position: Absolute — Breaking Free
Absolute positioning yanks an element completely out of the normal document flow. Other elements act as if it doesn't exist. The element then positions itself relative to its nearest positioned ancestor — meaning the closest parent that has position set to anything other than static.
If no ancestor is positioned? It positions itself relative to the <html> element. That's usually why your absolutely-positioned element ends up in a weird spot.
.tooltip {
position: absolute;
bottom: 100%; /* sits right above its positioned parent */
left: 50%;
transform: translateX(-50%); /* centers it horizontally */
background: #1e293b;
color: white;
padding: 8px 12px;
border-radius: 6px;
white-space: nowrap;
font-size: 0.85rem;
}
Pro tip: The bottom: 100% trick positions the element right above its parent. Similarly, top: 100% puts it directly below. Combine with left: 50% and transform: translateX(-50%) for perfect centering. This pattern is incredibly useful for dropdowns, tooltips, and popovers.
The Centering Trick
One of the most reliable centering techniques uses absolute positioning:
.centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* Or the modern stretch approach: */
.centered-alt {
position: absolute;
inset: 0;
margin: auto;
width: fit-content;
height: fit-content;
}
I prefer the inset: 0 + margin: auto approach these days. It's cleaner and works well for modals and overlays.
Position: Fixed — Glued to the Viewport
Fixed positioning is like absolute positioning, but the element positions itself relative to the viewport instead of a parent. It stays put when you scroll. Think headers, floating action buttons, cookie banners — anything that needs to stay visible.
.floating-btn {
position: fixed;
bottom: 24px;
right: 24px;
width: 56px;
height: 56px;
background: #3b82f6;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 4px 12px rgba(59,130,246,0.4);
font-size: 1.4rem;
z-index: 100;
}
One gotcha that trips people up: if any ancestor has a transform, perspective, or filter property set, fixed positioning breaks. The element will position relative to that ancestor instead of the viewport. I've spent embarrassing amounts of time debugging this one.
Position: Sticky — The Best of Both Worlds
Sticky is the newest addition and honestly my favorite. The element behaves like relative until you scroll past a specified threshold, then it behaves like fixed. It's perfect for section headers, table headers, and navigation bars.
.section-header {
position: sticky;
top: 0;
background: white;
padding: 16px 0;
border-bottom: 1px solid #e2e8f0;
z-index: 10;
}
The top: 0 means "stick when the top edge reaches 0px from the viewport's top." You must specify at least one of top, right, bottom, or left — otherwise sticky does nothing.
Why Your Sticky Element Isn't Sticking
This comes up so often it deserves its own section. Here are the usual suspects:
- Missing threshold — you forgot to set
top,bottom, etc. - Parent has
overflow: hiddenoroverflow: auto— this kills sticky positioning. The element sticks to the overflow container, not the viewport. - Parent has no scrollable height — if the parent element is exactly the same height as the sticky child, there's nowhere to stick.
- Flex or grid issues — sometimes a flex parent with
align-items: stretchgives the sticky element full container height, making it unable to stick.
Debugging tip: In Chrome DevTools, find the sticky element and check its parent's computed height. If the parent's height equals the element's height, that's your problem. The element needs room within its parent to actually scroll and stick.
Stacking Context and Z-Index: The Real Boss Fight
Here's where most developers hit a wall. You set z-index: 9999 and your modal is still behind the header. What gives?
The answer is stacking context. Z-index doesn't work globally — it works within stacking contexts. Think of a stacking context like a folder. Elements inside that folder compete with each other for stacking order, but the folder itself is a single unit in its parent's stacking order.
What Creates a Stacking Context?
- The root
<html>element - Any element with
positionset to absolute/relative/fixed/sticky AND a z-index other thanauto - Any element with
opacityless than 1 - Any element with
transform,filter,perspective, orbackdrop-filter - Flex/grid children with a z-index other than
auto - Elements with
isolation: isolate
A Practical Example
/* This modal will NEVER appear above .sidebar,
no matter how high you set its z-index */
.sidebar {
position: relative;
z-index: 2; /* Creates a stacking context */
}
.main-content {
position: relative;
z-index: 1; /* Creates another stacking context */
}
/* This modal is INSIDE .main-content's stacking context */
.modal {
position: fixed;
z-index: 999999; /* Doesn't matter! */
/* It's competing within z-index: 1's context */
}
The fix? Either move the modal outside of .main-content in the DOM, or remove the z-index from the parent containers so they don't create stacking contexts.
My Z-Index Strategy
I use a simple token system to keep z-index manageable across projects:
:root {
--z-dropdown: 100;
--z-sticky: 200;
--z-overlay: 300;
--z-modal: 400;
--z-toast: 500;
--z-tooltip: 600;
}
.dropdown { z-index: var(--z-dropdown); }
.header { z-index: var(--z-sticky); }
.overlay { z-index: var(--z-overlay); }
.modal { z-index: var(--z-modal); }
Gaps of 100 give you room to slot things in between later. And using CSS custom properties means you have a single source of truth. No more scattered z-index: 47 values that nobody remembers the reason for.
Common Patterns You'll Actually Use
Full-Width Overlay
.overlay {
position: fixed;
inset: 0; /* shorthand for top/right/bottom/left: 0 */
background: rgba(0, 0, 0, 0.5);
display: grid;
place-items: center;
z-index: var(--z-overlay, 300);
}
Sticky Table Header
thead th {
position: sticky;
top: 0;
background: #f8fafc;
box-shadow: 0 1px 0 #e2e8f0;
z-index: 1;
}
Aspect-Ratio Image Container (Old-School)
.image-container {
position: relative;
padding-bottom: 56.25%; /* 16:9 ratio */
overflow: hidden;
}
.image-container img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
(These days you can use aspect-ratio: 16/9 instead, but the padding-bottom trick is still useful to understand.)
Build Beautiful Visual Content
Need to turn your HTML layouts into shareable images? Convert any HTML — positioned elements and all — into crisp PNGs.
Try HTML to PNG Converter →Quick Reference: When to Use What
- Static — the default; you rarely write it explicitly
- Relative — small visual nudges, or as an anchor for absolute children
- Absolute — tooltips, badges, dropdowns, modals (elements that overlay others)
- Fixed — sticky headers, floating buttons, cookie banners (viewport-locked)
- Sticky — section headers, table headers, sidebar nav (scroll-aware sticking)
CSS positioning doesn't have to be a guessing game. Once you internalize that absolute positions relative to its nearest positioned ancestor, that sticky needs a threshold and scrollable parent, and that z-index only competes within stacking contexts — most layout mysteries solve themselves.
Next time something's in the wrong spot, check these three things: What's the nearest positioned ancestor? Is there an unexpected stacking context? Is overflow cutting off a sticky element? Nine times out of ten, one of those is the answer.