Why Positioning Matters
Every element on a web page occupies a position. By default, elements stack vertically in the order they appear in the HTML — this is normal document flow. CSS positioning lets you break out of this flow, overlap elements, pin content to the viewport, and build complex layered interfaces.
Understanding positioning is fundamental. It affects everything from dropdown menus and modals to sticky headers and tooltip placement.
The Five Position Values
CSS has five position values:
.element {
position: static; /* default — normal document flow */
position: relative; /* offset from normal position */
position: absolute; /* removed from flow, relative to positioned ancestor */
position: fixed; /* removed from flow, relative to viewport */
position: sticky; /* hybrid — normal flow until threshold, then fixed */
}
Each changes how the element relates to its surroundings and how top, right, bottom, left offsets behave.
position: static (Default)
Every element starts as static. It follows normal document flow. The top, right, bottom, left, and z-index properties have no effect on static elements.
.element {
position: static; /* default — usually not written explicitly */
top: 50px; /* IGNORED — has no effect on static elements */
}
Normal flow (all static):
+------------------+
| Element A |
+------------------+
| Element B |
+------------------+
| Element C |
+------------------+
position: relative
The element stays in normal flow but can be offset from its original position. The space it originally occupied is preserved — other elements don't shift.
.box {
position: relative;
top: 20px; /* pushes down 20px from original position */
left: 10px; /* pushes right 10px from original position */
}
Original position (dashed):
+- - - - - - -+
| (ghost) | <- space preserved
+- - - - - - -+
+----------------+
| Actual element | <- offset 20px down, 10px right
+----------------+
Key behaviors:
- The original space is still occupied (other elements don't move).
- Creates a positioning context for absolutely positioned children.
z-indexnow works (it doesn't on static elements).
Common use: creating a positioning context for an absolute child.
.parent {
position: relative; /* becomes positioning context */
}
.child {
position: absolute; /* now positions relative to .parent */
top: 0;
right: 0;
}
position: absolute
The element is removed from normal flow. It no longer takes up space. It positions itself relative to its nearest positioned ancestor (any ancestor with position other than static). If no positioned ancestor exists, it uses the initial containing block (usually the viewport).
.parent {
position: relative; /* creates positioning context */
width: 400px;
height: 300px;
}
.child {
position: absolute;
top: 10px; /* 10px from parent's top */
right: 10px; /* 10px from parent's right */
width: 100px;
height: 50px;
}
+----------------------------+
| .parent (relative) |
| +-------+ |
| | child | | <- positioned top-right
| +-------+ |
| |
+----------------------------+
The child is completely pulled out of flow. Siblings ignore it.
.container {
position: relative;
}
/* Badge on a card */
.badge {
position: absolute;
top: -8px;
right: -8px;
background: red;
border-radius: 50%;
width: 20px;
height: 20px;
}
position: fixed
Like absolute, but positions relative to the viewport (the browser window). The element stays in place even when the page scrolls.
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background: white;
z-index: 100;
}
+--viewport-------------------+
| [Fixed Navbar ]| <- stays here while scrolling
| |
| (page content scrolls |
| behind the navbar) |
| |
+-----------------------------+
Key behaviors:
- Removed from document flow.
- Positioned relative to viewport, not any parent.
- Stays in place during scroll.
- Creates a new stacking context.
Common uses: sticky headers, floating action buttons, modal overlays.
/* Fixed back-to-top button */
.back-to-top {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 50;
}
/* Full-screen modal overlay */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
Gotcha: position: fixed breaks when an ancestor has transform, filter, or will-change set. The fixed element becomes fixed relative to that ancestor instead of the viewport. This is a common source of bugs.
/* This breaks fixed positioning of children */
.ancestor {
transform: translateZ(0); /* creates containing block */
}
.child {
position: fixed; /* now fixed relative to .ancestor, not viewport */
}
position: sticky
A hybrid between relative and fixed. The element behaves as relative within its container until it reaches a scroll threshold, then it sticks like fixed.
.section-header {
position: sticky;
top: 0; /* sticks when it reaches the top of the viewport */
background: white;
z-index: 10;
}
Before scroll:
+--viewport-------------------+
| Other content |
| [Section Header] | <- position: relative
| Section content... |
+-----------------------------+
After scrolling past header:
+--viewport-------------------+
| [Section Header] | <- position: fixed (stuck)
| Section content... |
| More content... |
+-----------------------------+
After scrolling past the parent container:
+--viewport-------------------+
| Other content | <- header scrolls away with its parent
| [Next Section Header] |
+-----------------------------+
Requirements for sticky to work:
- Must specify at least one of
top,right,bottom,left. - Parent must not have
overflow: hidden,overflow: auto, oroverflow: scroll(in the scroll direction). - Parent must have enough height for scrolling.
/* Sticky sidebar */
.sidebar {
position: sticky;
top: 80px; /* sticks 80px from viewport top */
align-self: flex-start; /* important in flex containers */
}
/* Sticky table header */
.table-header th {
position: sticky;
top: 0;
background: white;
}
Position Value Comparison Table
| Property | In Flow? | Offset Reference | Scroll Behavior | Creates Stacking Context? |
|---|---|---|---|---|
| static | Yes | N/A (offsets ignored) | Scrolls with page | No |
| relative | Yes (space preserved) | Own original position | Scrolls with page | Yes (if z-index set) |
| absolute | No | Nearest positioned ancestor | Scrolls with positioned parent | Yes (if z-index set) |
| fixed | No | Viewport | Stays fixed | Always |
| sticky | Yes (until stuck) | Nearest scrollable ancestor | Sticks at threshold | Always |
z-index and Stacking Contexts
How z-index Works
z-index controls the stacking order of positioned elements (anything except static). Higher values appear on top.
.back { position: relative; z-index: 1; }
.middle { position: relative; z-index: 2; }
.front { position: relative; z-index: 3; } /* appears on top */
+--------+
| front | z-index: 3
+--------+ |
| middle | + z-index: 2
+--------+
| back | z-index: 1
+--------+
Stacking Contexts
A stacking context is a group of elements that are stacked together. An element with a stacking context acts as a boundary — its children can only compete for z-index within that context, not with elements outside it.
Things that create a new stacking context:
position+z-index(any value other thanauto)opacityless than 1transform(any value other thannone)filter(any value other thannone)will-change(with certain values)position: fixedorposition: stickyisolation: isolate
.parent-a {
position: relative;
z-index: 1; /* creates stacking context */
}
.child-a {
position: relative;
z-index: 9999; /* only stacks within parent-a's context */
}
.parent-b {
position: relative;
z-index: 2; /* creates stacking context */
}
.child-b {
position: relative;
z-index: 1; /* appears ABOVE child-a because parent-b > parent-a */
}
Visual result (counter-intuitive):
.child-a (z-index: 9999 inside context z:1)
is BELOW
.child-b (z-index: 1 inside context z:2)
Because parent-b (z:2) > parent-a (z:1)
This is the #1 source of z-index confusion. The child's z-index only matters within its parent's stacking context.
Debugging z-index
Common z-index debugging steps:
1. Is the element positioned? (z-index only works on non-static)
2. Are you comparing elements in the same stacking context?
3. Does a parent create an unexpected stacking context?
→ Check for: transform, opacity, filter, will-change
4. Use browser DevTools → Layers panel to visualize stacking
z-index Strategy
Instead of random high numbers, use a scale:
: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); }
.sticky-header { z-index: var(--z-sticky); }
.modal { z-index: var(--z-modal); }
This prevents z-index wars where developers keep increasing numbers.
Centering Techniques
Horizontal Centering
/* Block element with known width */
.center-block {
width: 300px;
margin-left: auto;
margin-right: auto;
}
/* Inline/inline-block element */
.parent {
text-align: center;
}
/* Flex */
.parent {
display: flex;
justify-content: center;
}
/* Grid */
.parent {
display: grid;
justify-items: center;
}
/* Absolute positioning */
.parent { position: relative; }
.child {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
Vertical Centering
/* Flex */
.parent {
display: flex;
align-items: center;
min-height: 200px;
}
/* Grid */
.parent {
display: grid;
align-items: center;
min-height: 200px;
}
/* Absolute positioning */
.parent { position: relative; min-height: 200px; }
.child {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
Both Axes (Perfect Center)
/* Method 1: Flexbox (recommended) */
.parent {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
/* Method 2: Grid (concise) */
.parent {
display: grid;
place-items: center;
min-height: 100vh;
}
/* Method 3: Absolute + transform */
.parent { position: relative; min-height: 100vh; }
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* Method 4: Absolute + inset + margin */
.parent { position: relative; min-height: 100vh; }
.child {
position: absolute;
inset: 0;
margin: auto;
width: 200px; /* needs explicit dimensions */
height: 100px;
}
/* Method 5: Grid + margin */
.parent {
display: grid;
min-height: 100vh;
}
.child {
margin: auto;
}
Centering Comparison
| Method | Known Dimensions? | Browser Support | Best For |
|---|---|---|---|
| Flexbox | No | Excellent | General centering |
| Grid place-items | No | Excellent | Single element centering |
| Absolute + transform | No | Excellent | Overlays, modals |
| Absolute + inset + margin | Yes | Excellent | Fixed-size elements |
| margin: auto (block) | Yes (width) | Universal | Horizontal centering only |
The inset Property
inset is shorthand for top, right, bottom, left:
/* These are equivalent */
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.overlay {
position: fixed;
inset: 0;
}
/* Asymmetric */
.sidebar {
position: fixed;
inset: 0 auto 0 0; /* top right bottom left */
width: 300px;
}
Positioning Gotchas
Gotcha 1: Absolute Inside Non-Positioned Parent
/* BUG: child positions relative to viewport, not parent */
.parent {
/* no position set — defaults to static */
}
.child {
position: absolute;
top: 0;
right: 0;
}
/* FIX: make parent a positioning context */
.parent {
position: relative; /* add this */
}
Gotcha 2: Fixed Positioning Broken by Transform
/* BUG: fixed element no longer fixed to viewport */
.grandparent {
transform: scale(1); /* creates containing block */
}
.fixed-element {
position: fixed; /* now relative to .grandparent, not viewport */
}
/* FIX: remove transform from ancestor, or restructure HTML */
This affects transform, filter, will-change, and perspective.
Gotcha 3: Sticky Not Sticking
/* BUG: sticky element doesn't stick */
.parent {
overflow: hidden; /* THIS breaks sticky */
}
.child {
position: sticky;
top: 0;
}
/* FIX: remove overflow from parent or restructure */
Other reasons sticky fails:
- No
top,right,bottom, orleftspecified. - Parent has no scrollable height (content shorter than container).
- An ancestor with
overflow: autooroverflow: scrollintercepts the scroll.
Gotcha 4: z-index Not Working
/* BUG: z-index has no effect */
.element {
z-index: 9999;
/* position is static (default) — z-index ignored */
}
/* FIX: add position */
.element {
position: relative; /* now z-index works */
z-index: 9999;
}
Gotcha 5: Absolute Element Collapsing Parent
/* BUG: parent has no height */
.parent {
position: relative;
background: blue;
}
.child {
position: absolute;
/* removed from flow — parent collapses to 0 height */
}
/* FIX: give parent explicit dimensions */
.parent {
position: relative;
min-height: 200px;
}
Gotcha 6: Fixed Elements and Mobile Keyboards
On mobile devices, when the virtual keyboard opens, position: fixed elements can shift unexpectedly. The viewport height changes but fixed elements may not reposition correctly.
/* Workaround for mobile */
.fixed-bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
}
/* Better on mobile: use sticky at the bottom */
.bottom-bar {
position: sticky;
bottom: 0;
}
Real-World Patterns
Dropdown Menu
.dropdown {
position: relative;
}
.dropdown-menu {
position: absolute;
top: 100%; /* right below the trigger */
left: 0;
min-width: 200px;
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: var(--z-dropdown, 100);
}
Tooltip
.tooltip-wrapper {
position: relative;
display: inline-block;
}
.tooltip {
position: absolute;
bottom: 100%; /* above the element */
left: 50%;
transform: translateX(-50%);
padding: 0.5rem;
background: #333;
color: white;
border-radius: 4px;
white-space: nowrap;
z-index: var(--z-tooltip, 600);
}
/* Arrow */
.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #333;
}
Overlay with Centered Modal
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: var(--z-modal, 400);
}
.modal {
background: white;
border-radius: 8px;
padding: 2rem;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
Sticky Header with Shadow on Scroll
.header {
position: sticky;
top: 0;
background: white;
z-index: var(--z-sticky, 200);
transition: box-shadow 0.2s;
}
/* Add shadow class via JS when scrolled */
.header.scrolled {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
Notification Badge
.icon-wrapper {
position: relative;
display: inline-block;
}
.badge {
position: absolute;
top: -4px;
right: -4px;
background: red;
color: white;
font-size: 0.7rem;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
Key Takeaways
staticis the default. Offsets and z-index don't apply.relativestays in flow but can be offset. Creates a positioning context for children.absoluteis removed from flow. Positions relative to the nearest positioned ancestor.fixedis removed from flow. Positions relative to the viewport. Stays put during scroll.stickyis a hybrid — relative until a scroll threshold, then sticks. Requirestop/right/bottom/left.z-indexonly works on positioned elements (notstatic).- Stacking contexts are the real key to understanding z-index. A child's z-index only matters within its parent's context.
- Use a z-index scale (CSS variables) to prevent z-index wars.
- Always make the parent
position: relativebefore usingposition: absoluteon a child. - Watch for
transform/filteron ancestors — they breakposition: fixed. - Watch for
overflowon ancestors — it breaksposition: sticky. - For centering: flexbox or grid are the easiest modern approaches.