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 positionstatic. 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 againstSelfNearest positioned ancestorViewportScroll container
Scrolls with pagePartially

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