CSS positioning is the system for placing elements at precise coordinates — either relative to their normal flow position, relative to a containing ancestor, or relative to the viewport. While Flexbox and Grid handle the vast majority of layout work, positioning is indispensable for tooltips, dropdowns, modals, sticky headers, and overlapping decorative elements. In this lesson you will master all five position values and know exactly when to reach for each one.
The Five Position Values
| Value | In Flow? | Offset From | Common Use Case |
|---|---|---|---|
static |
Yes — default | N/A — offsets ignored | Normal document flow (default) |
relative |
Yes — keeps its space | Its own normal-flow position | Establishing a positioning context for children |
absolute |
No — removed from flow | Nearest positioned ancestor (or viewport) | Tooltips, dropdowns, badges overlaying elements |
fixed |
No — removed from flow | Viewport — does not scroll | Sticky headers, floating buttons, cookie banners |
sticky |
Yes — until threshold | Scroll container — sticks at threshold | Sticky table headers, section labels |
Offset Properties
| Property | Moves Element | Logical Equivalent |
|---|---|---|
top |
Down from the top reference edge | inset-block-start |
right |
Left from the right reference edge | inset-inline-end |
bottom |
Up from the bottom reference edge | inset-block-end |
left |
Right from the left reference edge | inset-inline-start |
inset: 0 |
All four sides to 0 — fills the containing block | Shorthand for top/right/bottom/left |
Containing Block for absolute
| Ancestor Has | Is Containing Block? |
|---|---|
position: relative |
Yes |
position: absolute |
Yes |
position: fixed |
Yes |
position: sticky |
Yes |
position: static (default) |
No — skipped; browser looks further up |
position: relative without any offset values does nothing visually — the element stays in its normal-flow position. Its sole purpose when used without offsets is to establish a positioning context so that absolutely-positioned children anchor to it instead of a more distant ancestor.inset: 0 as shorthand for top: 0; right: 0; bottom: 0; left: 0. Combined with position: absolute on a child and position: relative on the parent, it creates a full overlay that exactly covers the parent — perfect for image overlays, loading states, and glass effects.position: sticky only works when the element has a defined threshold offset (e.g. top: 0) AND its scroll container has enough content for the element to actually stick. If a sticky element is not working, check: (1) does it have top/bottom set? (2) does its parent have overflow: hidden or overflow: auto — these break sticky positioning.Basic Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CSS Positioning</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; padding: 32px; background: #f8fafc; margin: 0; }
/* ── relative: offset from normal position ── */
.relative-demo {
position: relative;
top: 16px;
left: 24px;
background: #ede9fe;
padding: 12px 20px;
border-radius: 8px;
border: 2px solid #7c3aed;
display: inline-block;
margin-bottom: 32px; /* extra space because element is shifted down */
}
/* ── absolute: positioned relative to .card-host ── */
.card-host {
position: relative; /* establishes containing block */
background: white;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
width: 280px;
}
.card-badge {
position: absolute;
top: -10px;
right: -10px;
background: #ef4444;
color: white;
border-radius: 9999px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
border: 2px solid white;
}
.card-host h3 { margin: 0 0 8px; font-size: 1rem; }
.card-host p { margin: 0; font-size: 0.875rem; color: #64748b; }
/* ── Full overlay using inset: 0 ── */
.overlay-host {
position: relative;
width: 280px;
border-radius: 12px;
overflow: hidden;
margin-bottom: 24px;
}
.overlay-host img {
display: block;
width: 100%;
height: 160px;
object-fit: cover;
background: linear-gradient(135deg, #4f46e5, #7c3aed);
}
.overlay {
position: absolute;
inset: 0; /* top:0; right:0; bottom:0; left:0 */
background: linear-gradient(to top, rgba(0,0,0,0.7) 0%, transparent 60%);
display: flex;
align-items: flex-end;
padding: 16px;
}
.overlay-title { color: white; font-weight: 700; margin: 0; font-size: 1rem; }
/* ── sticky: sticks at top: 0 while scrolling ── */
.scroll-area {
height: 180px;
overflow-y: scroll;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: white;
}
.sticky-header {
position: sticky;
top: 0;
background: #4f46e5;
color: white;
padding: 8px 16px;
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.04em;
}
.scroll-item {
padding: 10px 16px;
border-bottom: 1px solid #f1f5f9;
font-size: 0.875rem;
color: #334155;
}
</style>
</head>
<body>
<p><strong>position: relative</strong> — shifted 16px down, 24px right from normal position:</p>
<div class="relative-demo">I am relatively positioned</div>
<p><strong>position: absolute</strong> — badge anchored to card corner:</p>
<div class="card-host">
<span class="card-badge">3</span>
<h3>Notifications Card</h3>
<p>The badge uses absolute positioning relative to the card's position: relative parent.</p>
</div>
<p><strong>inset: 0</strong> — gradient overlay covering parent exactly:</p>
<div class="overlay-host">
<div class="overlay-host img" style="height:160px;background:linear-gradient(135deg,#4f46e5,#7c3aed);"></div>
<div class="overlay"><p class="overlay-title">Gradient Overlay with inset: 0</p></div>
</div>
<p><strong>position: sticky</strong> — scroll the box below:</p>
<div class="scroll-area">
<div class="sticky-header">JANUARY 2025</div>
<div class="scroll-item">Meeting with design team</div>
<div class="scroll-item">Deploy v2.1 to production</div>
<div class="scroll-item">Quarterly review call</div>
<div class="sticky-header">FEBRUARY 2025</div>
<div class="scroll-item">Onboarding: new engineers</div>
<div class="scroll-item">Sprint planning session</div>
<div class="scroll-item">Launch blog post series</div>
<div class="sticky-header">MARCH 2025</div>
<div class="scroll-item">Conference: CSS Day</div>
<div class="scroll-item">Performance audit</div>
</div>
</body>
</html>
How It Works
Step 1 — relative Shifts Without Leaving Flow
position: relative; top: 16px; left: 24px moves the element’s painted position but its original slot in the document flow is preserved. Sibling elements do not fill in the vacated space — they still see the element as occupying its original position.
Step 2 — absolute Escapes Flow and Anchors to Parent
The notification badge is position: absolute; top: -10px; right: -10px. It climbs the DOM tree looking for the nearest ancestor with a non-static position — it finds .card-host with position: relative and anchors there. The -10px values push it outside the card’s border, creating the classic overlapping badge effect.
Step 3 — inset: 0 Creates a Perfect Overlay
inset: 0 is shorthand for all four offset properties set to zero. With position: absolute on the overlay and position: relative on the parent, the overlay stretches to exactly fill the containing block — covering the entire image without knowing its dimensions.
Step 4 — sticky Sticks at a Defined Threshold
The month header elements have position: sticky; top: 0. While scrolling, each header behaves like a normal block element. When it reaches the top of the scroll container (0px from the top), it “sticks” there — remaining visible until the next sticky header pushes it away.
Step 5 — fixed Would Pin to the Viewport
If the header used position: fixed; top: 0 instead of sticky, it would be removed from document flow entirely and always stay at the top of the viewport — even when the page is scrolled. This is the pattern for site-wide navigation bars.
Real-World Example: Modal with Backdrop
/* modal.css */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* Fixed backdrop covering the entire viewport */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(3px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 24px;
}
/* Modal box — centered by the flex backdrop */
.modal {
position: relative; /* for the close button */
background: white;
border-radius: 16px;
padding: 40px;
max-width: 480px;
width: 100%;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.2);
animation: slide-in 0.2s ease;
}
@keyframes slide-in {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
/* Absolutely positioned close button */
.modal-close {
position: absolute;
top: 16px;
right: 16px;
width: 32px;
height: 32px;
background: #f1f5f9;
border: none;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
color: #64748b;
transition: background 0.15s;
}
.modal-close:hover { background: #e2e8f0; color: #0f172a; }
.modal h2 { font-size: 1.25rem; margin-bottom: 12px; color: #0f172a; }
.modal p { color: #64748b; line-height: 1.6; font-size: 0.9rem; margin-bottom: 24px; }
.modal-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
/* Sticky site header */
.site-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 64px;
background: #ffffff;
border-bottom: 1px solid #e2e8f0;
display: flex;
align-items: center;
padding: 0 32px;
z-index: 100;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.06);
}
/* Compensate for fixed header height */
.page-content { padding-top: 64px; }
Common Mistakes
Mistake 1 — absolute child without positioned parent
❌ Wrong — child anchors to viewport instead of intended parent:
.card { /* no position set — defaults to static */ }
.card .badge { position: absolute; top: 0; right: 0; }
/* Badge positions relative to the next positioned ancestor — likely <body> */
✅ Correct — always add position: relative to the parent:
.card { position: relative; }
.badge { position: absolute; top: -10px; right: -10px; }
Mistake 2 — sticky not working due to overflow on parent
❌ Wrong — overflow: hidden on a parent breaks sticky positioning:
.container { overflow: hidden; } /* clips the sticky element's scroll context */
.sticky-el { position: sticky; top: 0; } /* won't stick */
✅ Correct — remove overflow: hidden from the sticky ancestor chain:
.container { /* overflow: hidden removed */ }
.sticky-el { position: sticky; top: 0; } /* now works correctly */
Mistake 3 — Forgetting padding-top to compensate for fixed header
❌ Wrong — page content hides behind the fixed header:
.header { position: fixed; top: 0; height: 60px; }
body { /* no top padding — content starts under the header */ }
✅ Correct — offset body or main content by header height:
.header { position: fixed; top: 0; height: 60px; }
body { padding-top: 60px; }
Quick Reference
| Value | Removes from Flow? | Reference Point | Scrolls with Page? |
|---|---|---|---|
static |
No (default) | N/A | Yes |
relative |
No | Own normal-flow position | Yes |
absolute |
Yes | Nearest positioned ancestor | Yes (with ancestor) |
fixed |
Yes | Viewport | No — fixed to viewport |
sticky |
No (until threshold) | Scroll container at threshold | Partially |
inset: 0 |
Shorthand | All four sides = 0 | With position type |