CSS Position Values Explained — static, relative, absolute, fixed, sticky
Quick answer
💡CSS position has five values: static (default, normal flow), relative (offset from normal position, creates context for absolute children), absolute (removed from flow, positioned relative to nearest non-static ancestor), fixed (positioned relative to viewport, broken by transform ancestors), and sticky (relative until scroll threshold, then fixed within its scroll container). z-index only works on non-static elements.
Error symptoms
- ✕
Absolutely positioned element anchors to the wrong parent or to the viewport - ✕
Fixed header scrolls away with the page instead of staying pinned - ✕
Sticky sidebar stops sticking partway down the page - ✕
z-index has no effect on an element - ✕
position:relative moves an element but takes up its original space - ✕
A transform on an ancestor breaks fixed positioning of a child element
Common causes
- •Absolute child has no positioned ancestor — positions relative to initial containing block (html element)
- •transform, filter, or will-change on an ancestor creates a containing block for fixed children, breaking viewport pinning
- •position:sticky has no offset value (top, left, right, or bottom) specified
- •An overflow:hidden ancestor cuts the scroll container chain for sticky elements
- •z-index applied to a static (non-positioned) element has no effect
- •position:sticky element has reached the end of its parent — it unsticks when parent leaves viewport
When it happens
- •Building modal dialogs where the close button must anchor to the modal card
- •Creating a fixed-top navigation that scrolls away on a page with a transform in the header
- •Adding a sticky table of contents sidebar to a long article
- •Layering elements with z-index in a complex UI and the order is wrong
- •Using position:relative to nudge an element and unexpectedly creating a new stacking context
Examples and fixes
Without a positioned ancestor, an absolute element anchors to the initial containing block — the html element.
Absolute child without positioned parent
❌ Wrong
/* close button goes to top-right of the entire page */
.modal {
background: white;
padding: 32px;
width: 480px;
margin: auto;
}
.modal__close {
position: absolute;
top: 16px;
right: 16px;
cursor: pointer;
}✅ Fixed
/* Add position:relative to create a containing block */
.modal {
position: relative; /* Now the containing block for .modal__close */
background: white;
padding: 32px;
width: 480px;
margin: auto;
}
.modal__close {
position: absolute;
top: 16px;
right: 16px;
cursor: pointer;
}An absolutely positioned element is removed from normal document flow and placed relative to its containing block — the nearest ancestor that has a position value other than static. When no ancestor is positioned, the element falls back to the initial containing block, which is the viewport or the html element depending on context. Adding position:relative to the modal container costs nothing in layout (relative elements stay in normal flow at their natural position) and creates the required containing block. This is the most common absolute positioning mistake and is fixed with one CSS property.
A CSS transform on any ancestor creates a containing block for fixed descendants, causing them to scroll with the page.
Fixed header broken by ancestor transform
❌ Wrong
/* Header should be fixed but scrolls with content */
.page-wrapper {
transform: translateX(0); /* Triggers containing block for fixed children */
}
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
/* Fixed relative to .page-wrapper, not the viewport */
}✅ Fixed
/* Move header outside the transform ancestor */
<div class="header">...</div>
<div class="page-wrapper">
<main>...</main>
</div>
/* Or use will-change sparingly — check if transform is removable */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
}The CSS specification states that transform, filter, perspective, and will-change: transform on an ancestor element create a new containing block for fixed-position descendants. A fixed element inside a transformed ancestor is no longer positioned relative to the viewport — it is positioned relative to the transformed element. The most reliable fix is to move the fixed header outside any transformed ancestor in the DOM. If the transform is added by a library or animation framework, check whether will-change can be removed after the animation completes using JavaScript.
position:sticky requires a top/bottom offset and a scrollable ancestor that is not clipping overflow.
Sticky sidebar that does not stick
❌ Wrong
/* Sticky without offset and inside overflow:hidden container */
.layout {
overflow: hidden;
display: flex;
gap: 24px;
}
.sidebar {
position: sticky;
/* Missing top — sticky never activates */
width: 240px;
}✅ Fixed
/* Provide top offset and ensure scroll ancestor is not clipping */
.layout {
display: flex;
gap: 24px;
/* Removed overflow:hidden */
align-items: flex-start; /* Prevents sidebar from stretching */
}
.sidebar {
position: sticky;
top: 24px; /* Required offset */
width: 240px;
}position:sticky requires two things that are frequently forgotten. First, a threshold offset must be specified — top, right, bottom, or left — indicating the distance from the scroll container edge at which the element becomes fixed. Without an offset, sticky never triggers. Second, the scroll container chain must not include an element with overflow:hidden, overflow:auto, or overflow:scroll, because those create a new scroll context. Also important: align-items:flex-start on the flex parent prevents the sidebar from stretching to the height of the main content, which would eliminate the scroll range over which sticky applies.
How each position value affects layout flow
CSS position controls whether an element participates in normal document flow, and if it is removed from flow, what reference frame it uses for its offset coordinates. The five values — static, relative, absolute, fixed, sticky — represent fundamentally different layout behaviours.
position:static is the default for every element. Static elements are placed by the normal flow algorithm. The top, right, bottom, and left offset properties have no effect on static elements. z-index also has no effect. Most layout problems involving z-index begin with an element that is still static when it should be non-static.
position:relative keeps the element in normal flow — it still occupies its original space in the document — but the element is visually offset by the specified top/right/bottom/left values. A relatively positioned element with top: 20px moves down 20px visually but the space it occupied before the offset remains reserved. Crucially, position:relative creates a new stacking context when combined with a z-index value, and it creates a containing block for absolutely positioned descendants.
position:absolute removes the element completely from normal flow. The surrounding elements reflow as if the absolute element does not exist. The element is then placed relative to its containing block — the nearest ancestor with position:relative, absolute, fixed, or sticky. If no such ancestor exists, the element uses the initial containing block, which corresponds to the html element and is roughly viewport-sized.
position:fixed also removes the element from flow, but positions it relative to the viewport. Scroll does not affect its position — it stays in the same viewport location regardless of page scroll. The containing block for fixed elements is normally the viewport, but transform, filter, perspective, or will-change:transform on any ancestor overrides this and makes that ancestor the containing block.
position:sticky is a hybrid: it behaves like position:relative until the element reaches the specified scroll threshold (e.g., top: 0), at which point it behaves like position:fixed within its scroll container. It reverts to relative when its parent leaves the viewport.
Diagnosing positioning bugs with DevTools
Open Chrome DevTools and select the misbehaving element in the Elements panel. In the Styles tab, confirm which position value is applied and check whether it is being overridden by a more specific rule. A struck-through position: absolute in the Styles tab indicates a winning rule that sets a different value.
For an absolute element that is anchoring to the wrong ancestor, you need to find the actual containing block. Select the absolute element, then in the Elements panel click up through its ancestors one by one. In the Styles tab, check each ancestor's position value in the Computed panel — look for any value other than static. The first non-static ancestor you encounter is the containing block.
For fixed elements that scroll with the page, the containing block is not the viewport. Add a temporary outline or background-color to confirm the element is fixed but to the wrong reference frame. Then traverse ancestors looking for transform, filter, will-change:transform, or perspective in the Computed tab. The first ancestor with any of these is capturing the fixed element as its containing block.
For sticky elements that do not stick, check two things in DevTools. First, confirm a top, right, bottom, or left offset is present — without it, sticky will not trigger. Second, inspect every ancestor for overflow values other than visible. The Layout panel in DevTools shows overflow badges on elements, making it easier to find which ancestor is creating a scroll container that traps the sticky element.
z-index problems are almost always caused by the element being static (z-index ignored) or being in a different stacking context from the element it should appear above. In DevTools, open the Layers panel to see the stacking context tree and identify which layer each element belongs to.
Targeted fixes for each position value
For an absolute child that anchors to the wrong place, add position:relative to the intended parent. This is always the first fix to try. position:relative on the parent costs nothing in terms of layout — the parent stays in normal flow at its natural position. Only its position value changes, establishing it as a containing block.
For a fixed element broken by an ancestor transform, move the fixed element to the top level of the DOM, outside any transformed ancestors. In React or Angular this typically means rendering the element via a portal into document.body. If the transform is applied for animation purposes and can be removed after the animation completes, remove it via JavaScript: element.style.transform = '' after the animation ends.
For a sticky element that does not stick, add a threshold offset (top: 0 or the desired distance) and audit every ancestor for overflow values other than visible. Replace overflow:hidden on ancestors with display:flow-root or a clearfix to contain floats without breaking the scroll container chain. Also ensure the sticky element's parent is tall enough that scroll occurs — if the parent's height equals the sticky element's height, there is no scroll range for sticky to operate within.
For z-index issues, add position:relative to the element if it is currently static. Only non-static elements respect z-index. If the element is already positioned but still appears below another element, check whether either element creates a new stacking context — a stacking context is self-contained and its children cannot appear above elements in a higher stacking context regardless of their z-index values.
For position:sticky in a flex or grid container, add align-items:flex-start or align-self:flex-start to the sticky element. By default, flex children stretch to the container's cross-axis size. A sidebar that stretches to the container height has no scroll range and sticky never activates.
Mobile and transform edge cases for positioning
On mobile devices, fixed positioning has historically been problematic in iOS Safari due to the browser chrome appearing and disappearing. When the address bar collapses, the viewport height changes, causing fixed elements anchored to the bottom of the viewport to jump. The modern solution is to use environment variables: bottom: env(safe-area-inset-bottom) to account for safe area insets on notched devices.
Some CSS animations use transform:translateZ(0) or will-change:transform as performance hints to promote an element to its own compositor layer. When this hint is applied to an ancestor, it becomes a containing block for fixed descendants inside it. This is a frequent source of confusion — a developer adds a performance hint to a wrapper and the fixed header breaks. Always test fixed positioning when adding transform hints to layout wrappers.
In a CSS Grid layout, position:absolute children of a grid container are positioned relative to the grid container if it is the nearest positioned ancestor. But they do not participate in grid placement — they overlap other grid items unless placed explicitly with grid-column and grid-row. This can be used intentionally to overlay elements within a grid cell.
position:sticky in a horizontal scroll container sticks to the horizontal scroll position, not the vertical scroll. Horizontal tables with sticky first columns use overflow-x:auto on the table wrapper and position:sticky; left:0 on the first column cells. The sticky column stays in view as the user scrolls the table horizontally.
In Safari, position:sticky on a table cell (th or td) has had intermittent support. For sticky table headers, test specifically in Safari and use a polyfill or pure JavaScript approach if sticky does not work reliably. Alternatively, use a div-based table layout where sticky is fully supported.
Position property mistakes that cause layout failures
The single most common mistake is forgetting to add position:relative to the containing parent of an absolutely positioned child. The result is the absolute element flying to the corner of the page. Every time you write position:absolute, immediately ask: what is the containing block and does it have a non-static position?
Using position:fixed for elements that should be sticky. Fixed elements are always positioned relative to the viewport regardless of scroll position. Sticky elements are positioned relative to their scroll container and stop at the parent's boundary. A sticky table of contents sidebar should use sticky, not fixed — a fixed sidebar would require precise top and height calculations and would not naturally stop at the end of the article.
Applying z-index to a static element expecting it to layer over other elements. z-index does nothing on static elements. Always pair z-index with a position value. The minimum needed is position:relative — this has no visual side effect on layout but enables z-index.
Using negative margins or translate to visually move an element outside its parent, then expecting overflow:hidden not to clip it. Negative margins and translate move the visual output of the element outside the border box, and overflow:hidden clips at the border box. Either remove overflow:hidden or keep the element's visual position within the container.
Forgetting that position:sticky requires the parent to be taller than the sticky element to have a scroll range. If a sidebar is the only content in a flex container and the flex container's height matches the sidebar, sticky has no room to operate. The container needs content that scrolls — typically the article body alongside the sidebar.
Positioning patterns for maintainable layouts
Prefer modern layout primitives — Flexbox and Grid — for most layout work. Use position only when you need elements outside normal flow: overlays, tooltips, fixed navigation, sticky sidebars. Minimising positioned elements reduces the complexity of stacking contexts and containing block chains.
Always document the position:relative parent when writing the absolutely positioned child. A comment like /* contains .modal__close, .modal__overlay */ on the relative parent prevents future developers from removing it without understanding the dependency. This simple comment saves hours of debugging.
For complex layered UIs with many z-index values, maintain a z-index scale in a token file. Use named values: z-default: 1, z-dropdown: 100, z-modal: 200, z-toast: 300. Referencing tokens prevents arbitrary escalation and makes the layering intent clear. Establish the scale early — retrofitting z-index order after the fact is difficult.
Test fixed and sticky positioning on real iOS Safari. Safari has had bugs with both behaviours and handles viewport units, address bar size, and safe areas differently from Chrome. Tools like BrowserStack or a physical iPhone are worth the friction to catch Safari-specific regressions before users do.
Use the CSS Formatter tool to audit layout CSS before shipping. Formatting makes it easier to spot missing position:relative declarations on parents, z-index values applied to static elements, and sticky elements missing offset values. Consistent formatting reduces the cognitive load of reviewing positioning rules in code review.
For internationalized layouts that must support right-to-left languages, replace physical offset properties with CSS logical properties. Instead of top, right, bottom, and left, use inset-block-start, inset-inline-end, inset-block-end, and inset-inline-start — or the inset shorthand, which maps to all four sides at once. Logical properties automatically flip when the document direction changes to rtl, eliminating the need for separate overrides inside a [dir="rtl"] selector and making RTL-safe positioning the default rather than an afterthought.
CSS position debug checklist
- ✓Confirm position:relative on the intended parent of every absolute child
- ✓Check for transform, filter, or will-change:transform on ancestors of fixed elements
- ✓Add a top/bottom/left/right offset to every position:sticky element
- ✓Remove overflow:hidden from ancestors of sticky elements — use display:flow-root instead
- ✓Add position:relative or another non-static value to elements that need z-index
- ✓Add align-self:flex-start to sticky elements inside flex containers
- ✓Test fixed and sticky positioning on real iOS Safari, not only desktop Chrome
- ✓Check the stacking context tree in DevTools Layers panel for z-index conflicts
Related guides
Frequently asked questions
What is the difference between position:absolute and position:fixed?
Both remove the element from normal document flow, but they use different reference frames. position:absolute positions the element relative to its nearest non-static ancestor — as the page scrolls, the element scrolls with its containing block. position:fixed positions the element relative to the viewport — it stays in the same screen position regardless of scrolling. Fixed positioning is broken by transform, filter, or will-change:transform on any ancestor, which makes that ancestor the reference frame instead of the viewport.
Why does z-index not work on my element?
z-index only affects elements with a position value other than static. Static elements ignore z-index entirely. Add position:relative, which keeps the element in normal flow but activates z-index. If the element is already positioned but still appears behind another element, both elements may be in different stacking contexts. Stacking contexts are self-contained — a high z-index inside one context cannot appear above a low z-index in a higher context.
How do I make position:sticky work?
Sticky requires three things: a threshold offset (top, right, bottom, or left with a value), a scroll container that is not clipping overflow (overflow must be visible on all ancestors in the scroll chain), and a parent element taller than the sticky element so there is a scroll range. Missing any one of these causes sticky to behave like position:relative. In a flex container, also add align-self:flex-start to prevent the sticky element from stretching to the container height.
Why is my fixed element not staying in place when I scroll?
A fixed element that scrolls with the page has a transformed ancestor intercepting it. transform, filter, perspective, or will-change:transform on any ancestor creates a containing block for fixed descendants. The fixed element is then positioned relative to that ancestor, not the viewport. Move the fixed element higher in the DOM, outside any transformed ancestors, or remove the transform once the animation is complete. In frameworks, use a portal to render fixed elements at the document body level.
What is a containing block in CSS positioning?
The containing block is the reference box that positioned elements use for their offset coordinates and percentage dimensions. For position:absolute, the containing block is the nearest ancestor with a position other than static. For position:fixed, it is normally the viewport, but transform or filter on an ancestor overrides this. For position:sticky, it is the nearest scroll container. For position:static and position:relative, it is the element's parent in normal flow.
Does position:relative move an element?
Visually yes, but the element's original space is preserved. position:relative with top:20px moves the element 20px down visually, but the document flow treats the element as if it were still at its original position. Other elements do not reflow to fill the gap. Use margin or padding to shift elements without preserving their original space. Use position:relative when you need to create a containing block for absolute children or enable z-index, even if you do not specify any offset.
How does position:sticky interact with a flex container?
In a flex container, sticky elements stretch to match the cross-axis size of the container by default (align-items:stretch). A sidebar stretched to the height of the main content has no scroll range, so sticky never activates. Fix by adding align-items:flex-start to the flex container or align-self:flex-start to the sticky element. This allows the sidebar to be shorter than the content, creating the scroll range that sticky requires.
What creates a new stacking context in CSS?
A stacking context is created by: position:relative or absolute with a z-index other than auto; position:fixed or sticky; opacity less than 1; transform other than none; filter other than none; will-change referencing any of these; isolation:isolate; mix-blend-mode other than normal. Elements inside a stacking context are painted together and cannot interleave with elements outside it. This is why a high z-index inside a low-z-index context can still appear below elements in other contexts.
Can I use position:sticky in a table for sticky columns?
Yes. Apply position:sticky and left:0 to table header cells (th) or the first data cell (td) in each row to create a sticky first column. The table wrapper needs overflow-x:auto to create the horizontal scroll container. Sticky table headers use position:sticky and top:0 on the thead cells. Safari has had intermittent support for sticky table cells — test thoroughly in Safari and have a JavaScript fallback if needed for sticky table columns.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.