Suggest an editImprove this articleRefine the answer for “CSS position property”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**`position`** controls where an element sits on the page and whether it holds space in the document flow. ```css .parent { position: relative; } /* positioning context */ .tooltip { position: absolute; top: 0; } /* anchors to .parent */ .navbar { position: fixed; top: 0; } /* stays on scroll */ ``` **Key:** `static`/`relative` stay in flow (space reserved). `absolute`/`fixed` leave it (overlap freely). `sticky` does both, depending on scroll position.Shown above the full answer for quick recall.Answer (EN)Image**`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 | 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** ```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.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.