CSS position property
position is a CSS property that controls where an element sits on the page and whether it takes up space in the document's normal flow.
Theory
TL;DR
- Normal flow is like assigned seats in a theater.
staticis your seat.relativenudges you sideways but keeps the seat reserved.absoluteandfixedput you on stage entirely. staticandrelativehold space in the flow.absoluteandfixeddo not.- Use
relativeon a parent to anchor positioned children. Useabsolutefor overlays. Usefixedfor navbars that stay on screen. stickyflows normally, then pins at a scroll threshold you define.
Quick example
.container {
position: relative; /* creates a positioning context */
height: 100px;
}
/* Stays in flow, ignores top/left */
.static-box { position: static; }
/* Shifts 20px down but leaves a gap where it was */
.nudged { position: relative; top: 20px; }
/* Leaves the flow, anchors to .container */
.overlay {
position: absolute;
top: 10px;
left: 10px;
}static is the default. Any other value activates offset properties (top, left, etc.) and changes how the browser calculates the element's position.
Key difference
static and relative sit in the flow, so sibling elements make room for them. Shift a relative element 50px down and a visible gap appears above it. absolute and fixed are pulled out entirely: they float above the page and siblings act as if they do not exist. sticky starts as relative and switches behavior once it crosses the scroll edge you define.
When to use
- Default layout with no offsets needed:
static - Minor visual nudge without breaking flow:
relative - Container that anchors positioned children:
relativeon the parent - Tooltip, dropdown, badge, or modal:
absoluteinside arelativewrapper - Navbar or cookie banner that stays visible on scroll:
fixed - Table header or sidebar that scrolls with content then sticks:
sticky
Comparison table
| Value | In flow? | Reference point | top/left work? | Scrolls away? | Use for |
|---|---|---|---|---|---|
static | Yes | Normal flow | No | Yes | Default layout |
relative | Yes | Own original position | Yes | Yes | Small offsets, parent context |
absolute | No | Nearest positioned ancestor or body | Yes | Yes | Overlays, dropdowns, badges |
fixed | No | Viewport | Yes | No | Navbars, banners, toasts |
sticky | Yes (until threshold) | Normal flow, then scroll edge | Yes | No (at edge) | Table headers, sidebars |
How browsers calculate position
Browsers build a layout tree during rendering. static and relative elements get coordinates inside the normal block or inline formatting context. absolute and fixed form a separate containing block: the engine walks up the ancestor chain looking for the first element with a position value other than static. For fixed, it falls back to the viewport if none is found.
sticky works differently. It flows normally, and the browser tracks its offset during scroll. Once the element hits the threshold you set, it pins to the scroll container's edge. One non-obvious thing: if the scroll container has overflow: auto or overflow: hidden, sticky pins to that container, not the viewport. I have seen this catch teams off-guard when they expected sticky header behavior inside a scrollable div.
z-index only applies to positioned elements (anything except static). Setting z-index: 10 on a static element does nothing.
Common mistakes
absolute without a positioned parent
/* Wrong: element jumps to the body edge */
.element { position: absolute; top: 0; left: 0; }
/* Right: wrap it in a relative container */
.container { position: relative; }
.element { position: absolute; top: 0; left: 0; }The element climbs the DOM until it finds a positioned ancestor. If none exists, it lands at body. Almost never what you want.
z-index on a static element
/* Wrong: completely ignored */
.card { z-index: 10; }
/* Right: needs a positioning context first */
.card { position: relative; z-index: 10; }z-index only works on positioned elements. Without position, the property has zero effect.
fixed inside overflow: hidden or overflow: auto
/* Wrong: the fixed element gets clipped to the parent bounds */
.panel { overflow: auto; }
.toast { position: fixed; bottom: 20px; right: 20px; }
/* Right: move it outside the overflow container */
body > .toast { position: fixed; bottom: 20px; right: 20px; }A transform, filter, or will-change property on any ancestor also creates a new containing block, which silently breaks fixed positioning.
sticky with no height on the parent
If the parent collapses or has no height, sticky has no scroll room and never sticks. The parent container has to be tall enough for scrolling to actually happen.
Real-world usage
- React modals: outer
divwithposition: fixedcovers the full viewport (top: 0, left: 0, right: 0, bottom: 0, zIndex: 1000), innerdivwithposition: absolutecenters viatransform: translate(-50%, -50%) - Material UI AppBar:
position: fixedby default, page content compensates withmargin-top - Bootstrap badge on a button:
position: relativeon the button,position: absoluteon the badge withtop: -8px, right: -8px - Headless UI / Tailwind tables:
stickyonthead thfor scrollable table headers - VS Code sidebar:
fixedinside an Electron viewport
Follow-up questions
Q: What is the difference between fixed and sticky?
A: fixed is always locked to the viewport regardless of scroll position. sticky lives in the normal flow first, then pins to the scroll edge once the threshold is crossed. Unlike fixed, it stops moving when the parent container scrolls out of view.
Q: What creates a containing block for absolute?
A: Any ancestor with position other than static. Also: any ancestor with transform, filter, perspective, will-change: transform, or contain: layout creates a new containing block. This is the source of most "my fixed element is not fixed" bugs.
Q: Why does z-index not work on my element?
A: Two common reasons. First, the element is position: static, so set any other position value. Second, the element is inside a stacking context with a lower z-index than a sibling context. A parent with opacity < 1, transform, or isolation: isolate creates a new stacking context, and its children can never stack above elements outside it.
Q: If I use percentage offsets with absolute, what is the reference size?
A: The containing block's dimensions, not the parent's content area. So top: 50% means 50% of the nearest positioned ancestor's height.
Q: Does sticky work inside flexbox or grid?
A: Yes, but the sticking axis depends on scroll direction. align-self: stretch (the flex default) can also affect when the threshold is hit.
Q: Explain stacking contexts and paint order.
A: A stacking context is an isolated rendering layer. It is created by position: relative or absolute with a non-auto z-index, opacity < 1, transform, filter, isolation: isolate, and a few others. Within a context, children paint in z-index order. But a child can never escape its parent context: if two stacking contexts are siblings, the one whose parent has the higher z-index always paints on top, no matter what values the children inside carry. This is why two modals from different parts of the DOM can overlap in unexpected ways.
Examples
Basic: five values side by side
<style>
.container { position: relative; height: 120px; background: #e8f4f8; }
.box { padding: 4px 8px; font-size: 14px; }
.static-box { background: #e74c3c; }
.relative-box { background: #2ecc71; position: relative; top: 20px; }
.absolute-box { background: #e67e22; position: absolute; top: 8px; right: 8px; }
</style>
<div class="container">
<div class="box static-box">static: normal spot</div>
<div class="box relative-box">relative: 20px gap above</div>
<div class="box absolute-box">absolute: top-right of container</div>
</div>The red box sits at its natural spot. The green one shifts down and leaves a gap. The orange one lifts out of the flow entirely and anchors to the container corner.
Intermediate: React modal overlay
function Modal({ isOpen, children }) {
if (!isOpen) return null;
return (
// Covers entire viewport, blocks page interaction
<div style={{
position: 'fixed',
top: 0, left: 0, right: 0, bottom: 0,
background: 'rgba(0,0,0,0.5)',
zIndex: 1000
}}>
{/* Centers dialog regardless of its size */}
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: '#fff',
padding: '24px',
borderRadius: '8px'
}}>
{children}
</div>
</div>
);
}The outer fixed layer stays on screen during scroll and blocks the page. The inner absolute layer uses transform to center itself regardless of the dialog's size.
Advanced: sticky header inside overflow
.container { height: 200vh; overflow: auto; }
.header { position: sticky; top: 0; background: #f1c40f; padding: 8px; }
.content { height: 100vh; padding: 16px; }<div class="container">
<div class="header">Sticky header</div>
<div class="content">Lots of content...</div>
</div>The header sticks to the .container scroll edge, not the viewport. Drop this .container inside another element with overflow: hidden and the header stops sticking altogether. Two levels of overflow and sticky breaks in ways that take a while to debug.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.