CSS Positioning: Complete Deep Dive
The position property is one of the most powerful — and most misunderstood — tools in CSS. It controls how elements are removed from the normal document flow and placed precisely.
1. The Five Position Values
static (Default)
Every element starts as static. It follows normal document flow. top, right, bottom, left, and z-index have no effect.
div { position: static; } /* This is the default — you never need to write it */
relative
The element stays in normal flow — its original space is preserved. top/right/bottom/left move it from its natural position, without affecting other elements.
.shifted {
position: relative;
top: 20px; /* Moved 20px DOWN from its normal position */
left: 10px; /* Moved 10px RIGHT from its normal position */
/* The space it would have occupied still exists */
}
Primary use case: Creating a positioning context (coordinate origin) for absolutely positioned children:
.card { position: relative; } /* Parent: coordinate anchor */
.card__badge { position: absolute; top: 12px; right: 12px; } /* Child: anchors to card */
absolute
Removed from normal flow — takes up no space. Positioned relative to its nearest ancestor with position ≠ static. If none exists, it positions against the <html> element (the initial containing block).
/* Pattern: parent relative + child absolute */
.image-wrapper {
position: relative; /* The anchor */
width: 400px;
}
.image-wrapper__overlay {
position: absolute;
inset: 0; /* Shorthand: top: 0; right: 0; bottom: 0; left: 0 */
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
}
.image-wrapper:hover .image-wrapper__overlay { opacity: 1; }
fixed
Like absolute but positioned relative to the viewport — not any ancestor. Stays in place regardless of scrolling. Removed from normal flow.
/* Sticky header */
.site-nav {
position: fixed;
top: 0;
left: 0;
right: 0; /* Stretch full width */
z-index: var(--z-sticky, 200);
height: 70px;
}
/* Compensate for fixed header pushing content down */
body { padding-top: 70px; }
/* Or use this to avoid manual padding-top */
.site-nav + main { margin-top: 70px; }
sticky
The element behaves as relative until it hits a specified scroll threshold — then it becomes fixed within its scroll container until its parent scrolls out of view.
/* Classic sticky header */
header {
position: sticky;
top: 0; /* Sticks when it reaches the top of the viewport */
z-index: 100; /* Must be above content it scrolls over */
}
/* Sticky sidebar (within a scrollable content area) */
.sidebar {
position: sticky;
top: 80px; /* Sticks 80px from the top (accounts for fixed nav) */
align-self: flex-start; /* Essential in Flexbox context */
max-height: calc(100vh - 80px);
overflow-y: auto;
}
/* Sticky table headers */
th {
position: sticky;
top: 0;
background: var(--color-surface); /* Must have background or shows through */
z-index: 1;
}
2. Position Values Comparison Table
static | relative | absolute | fixed | sticky | |
|---|---|---|---|---|---|
| In normal flow | ✅ | ✅ | ❌ | ❌ | ✅ (until stuck) |
| Takes up space | ✅ | ✅ | ❌ | ❌ | ✅ |
| top/left/etc | ❌ | ✅ | ✅ | ✅ | ✅ |
| z-index works | ❌ | ✅ | ✅ | ✅ | ✅ |
| Positions against | — | Self | Nearest positioned ancestor | Viewport | Scroll container |
| Scrolls with page | ✅ | ✅ | ✅ | ❌ | Partially |
3. The inset Shorthand
inset is shorthand for top, right, bottom, left together:
/* These are equivalent */
.overlay { top: 0; right: 0; bottom: 0; left: 0; }
.overlay { inset: 0; }
/* Directional shorthand (same as margin/padding) */
.panel { inset: 20px 0; } /* 20px top/bottom, 0 left/right */
.modal { inset: 10% 5%; } /* Centered with percentage insets */
4. Common Absolutely Positioned Patterns
Corner Badge
.card { position: relative; }
.card__badge { position: absolute; top: 12px; right: 12px;
background: var(--color-primary); color: white;
padding: 2px 8px; border-radius: 99px; font-size: 0.75rem; }
Tooltip (Absolute)
.tooltip-trigger { position: relative; }
.tooltip {
position: absolute;
bottom: calc(100% + 8px); /* Just above the trigger */
left: 50%; transform: translateX(-50%);
background: #333; color: white;
padding: 6px 12px; border-radius: 4px;
white-space: nowrap; pointer-events: none;
opacity: 0; transition: opacity 0.2s;
}
.tooltip-trigger:hover .tooltip { opacity: 1; }
Full-Screen Modal (Fixed)
.modal-backdrop {
position: fixed;
inset: 0; /* Covers entire viewport */
background: rgba(0,0,0,0.7);
z-index: var(--z-overlay, 300);
display: flex;
align-items: center;
justify-content: center;
}
.modal-dialog {
position: relative; /* For close button inside */
background: var(--color-surface);
border-radius: var(--radius-lg);
max-width: min(90vw, 640px);
padding: 2rem;
z-index: var(--z-modal, 400);
}
5. sticky Gotchas
The most common reasons position: sticky doesn't work:
| Problem | Cause | Fix |
|---|---|---|
| Doesn't stick | Missing top/left value | Always specify top: 0 (or whatever offset) |
| Doesn't stick | Parent has overflow: hidden | Remove overflow from ancestors (use overflow: clip if needed) |
| Doesn't stick | Parent is too small | Sticky needs scrollable content longer than the container |
| Sticks wrong spot | Wrong top value | Adjust for any fixed nav height: top: 80px |