Skip to main content

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. static is your seat. relative nudges you sideways but keeps the seat reserved. absolute and fixed put you on stage entirely.
  • static and relative hold space in the flow. absolute and fixed do not.
  • Use relative on a parent to anchor positioned children. Use absolute for overlays. Use fixed for navbars that stay on screen.
  • sticky flows normally, then pins at a scroll threshold you define.

Quick example

css
.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: relative on the parent
  • Tooltip, dropdown, badge, or modal: absolute inside a relative wrapper
  • Navbar or cookie banner that stays visible on scroll: fixed
  • Table header or sidebar that scrolls with content then sticks: sticky

Comparison table

ValueIn flow?Reference pointtop/left work?Scrolls away?Use for
staticYesNormal flowNoYesDefault layout
relativeYesOwn original positionYesYesSmall offsets, parent context
absoluteNoNearest positioned ancestor or bodyYesYesOverlays, dropdowns, badges
fixedNoViewportYesNoNavbars, banners, toasts
stickyYes (until threshold)Normal flow, then scroll edgeYesNo (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

css
/* 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

css
/* 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

css
/* 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 div with position: fixed covers the full viewport (top: 0, left: 0, right: 0, bottom: 0, zIndex: 1000), inner div with position: absolute centers via transform: translate(-50%, -50%)
  • Material UI AppBar: position: fixed by default, page content compensates with margin-top
  • Bootstrap badge on a button: position: relative on the button, position: absolute on the badge with top: -8px, right: -8px
  • Headless UI / Tailwind tables: sticky on thead th for scrollable table headers
  • VS Code sidebar: fixed inside 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

html
<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

jsx
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

css
.container { height: 200vh; overflow: auto; } .header { position: sticky; top: 0; background: #f1c40f; padding: 8px; } .content { height: 100vh; padding: 16px; }
html
<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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?