Skip to main content

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​

staticrelativeabsolutefixedsticky
In normal flow✅✅❌❌✅ (until stuck)
Takes up space✅✅❌❌✅
top/left/etc❌✅✅✅✅
z-index works❌✅✅✅✅
Positions against—SelfNearest positioned ancestorViewportScroll 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:

ProblemCauseFix
Doesn't stickMissing top/left valueAlways specify top: 0 (or whatever offset)
Doesn't stickParent has overflow: hiddenRemove overflow from ancestors (use overflow: clip if needed)
Doesn't stickParent is too smallSticky needs scrollable content longer than the container
Sticks wrong spotWrong top valueAdjust for any fixed nav height: top: 80px