Skip to main content

CSS Positioning

CSS positioning controls how an element is placed relative to normal flow, its own original position, a containing block, a viewport or a scrolling area. The position property supports five commonly used values: static, relative, absolute, fixed and sticky.

Normal flow
├── static -> Normal position
├── relative -> Normal position, then visually offset
└── sticky -> Normal flow until a scrolling threshold

Out of normal flow
├── absolute -> Positioned from a containing block
└── fixed -> Usually positioned from the viewport

One-line idea: Choose a positioning mode by deciding whether the element should keep its place in normal flow and which box should control its offsets.

Core Concepts​

Start with Normal Flow​

Before applying positioning, the browser places elements in normal flow according to their display type and layout context.

<h1>Products</h1>
<p>Browse the latest items.</p>
<button>View cart</button>

Block elements usually stack in the block direction, while inline content flows within lines. Flexbox and Grid also define structured layout flows.

Positioning should solve a placement need, not replace Flexbox or Grid for an entire page layout.

Inset Properties​

Positioned elements can use physical inset properties:

.box {
top: 1rem;
right: 2rem;
bottom: auto;
left: 0;
}

The inset shorthand can set all four sides:

.overlay {
position: absolute;
inset: 0;
}

Logical inset properties adapt to writing direction:

.badge {
position: absolute;
inset-block-start: 0.5rem;
inset-inline-end: 0.5rem;
}

Properties such as inset-inline-end are often preferable to hard-coding right in components that support both left-to-right and right-to-left languages.

Position Values​

position: static​

static is the default. The element participates in normal flow, and the inset properties do not reposition it.

.article {
position: static;
}
Before: Heading -> Article -> Footer
After: Heading -> Article -> Footer

Use it for ordinary content that does not need positioned offsets.

position: relative​

A relatively positioned element keeps its original space in normal flow, then is visually offset from that position.

.label {
position: relative;
inset-block-start: 0.25rem;
}
Layout reserves the original space
↓
Element is painted at an offset
↓
Nearby elements do not move into the old space

Common uses include:

  • small visual offsets
  • creating a containing block for positioned descendants
  • participating in stacking with z-index

Avoid using relative offsets to repair a layout that should instead use spacing, alignment, Flexbox or Grid.

position: absolute​

An absolutely positioned element is removed from normal flow. Its offsets are calculated from its containing block.

<article class="product-card">
<img src="shoe.webp" alt="Blue running shoe" />
<span class="badge">New</span>
</article>
.product-card {
position: relative;
}

.badge {
position: absolute;
inset-block-start: 0.75rem;
inset-inline-end: 0.75rem;
}

The positioned card establishes the containing block for the badge. The badge occupies no normal-flow space, so other content behaves as though it is not there.

Common uses include:

  • notification badges
  • controls layered over media
  • decorative elements
  • menus and popovers when CSS anchoring or top-layer APIs are not being used

position: fixed​

A fixed element is removed from normal flow and is usually positioned relative to the viewport:

.back-to-top {
position: fixed;
inset-inline-end: 1rem;
inset-block-end: 1rem;
}

It normally remains at the same viewport position while the document scrolls.

Common uses include:

  • floating action buttons
  • persistent toolbars
  • back-to-top controls
  • viewport-level overlays

Fixed positioning has an important exception: properties such as transform, perspective or filter on an ancestor can establish the fixed element's containing block. In that case, it may behave as fixed to that ancestor rather than to the viewport.

position: sticky​

A sticky element participates in normal flow. It behaves like a relatively positioned element until scrolling would move it beyond a specified inset threshold; it is then constrained within its scrolling area and containing block.

.section-heading {
position: sticky;
inset-block-start: 0;
background: white;
}
Element scrolls in normal flow
↓
Reaches the top inset threshold
↓
Stays against that edge while allowed
↓
Stops at its containing boundary

At least one inset on the sticky axis must be non-auto. Without top, bottom or the corresponding logical inset, sticky positioning behaves like relative positioning on that axis.

Common uses include:

  • section headings
  • table headers
  • filter or navigation sidebars
  • controls that should remain visible within a panel

Positioning Comparison​

ValueIn normal flow?Insets apply?Offset referenceTypical use
staticYesNoNormal layoutStandard content
relativeYes; original space remainsYesIts normal positionAnchor or small offset
absoluteNoYesContaining blockBadge, overlay or precise layer
fixedNoYesUsually viewportPersistent viewport control
stickyYesYesScroll container and containing blockSticky header or sidebar

Quick Mental Model​

static   = Keep me in normal flow
relative = Keep my space, then offset me
absolute = Remove my space and place me in a containing block
fixed = Keep me at a viewport position, usually
sticky = Keep my space, then constrain me during scrolling

Containing Blocks​

Why the Containing Block Matters​

Offsets and percentage sizes are resolved against a reference box called the containing block. It is not always simply the direct parent.

For a typical absolutely positioned element, the containing block is established by the nearest ancestor whose position is not static:

<section class="panel">
<div class="content">
<button class="close">Close</button>
</div>
</section>
.panel {
position: relative;
}

.close {
position: absolute;
top: 0;
right: 0;
}

Even though .content is the direct parent, .panel is the positioned ancestor and therefore the typical containing block.

Other properties, including transforms and containment, can also establish containing blocks. Inspect the ancestor chain when an element positions itself relative to an unexpected box.

Stretching an Absolutely Positioned Element​

When width is auto, setting both horizontal insets can stretch a non-replaced absolutely positioned element across the available space:

.banner {
position: absolute;
left: 1rem;
right: 1rem;
}

The same idea applies vertically with top and bottom when height remains auto.

Sticky Positioning in Practice​

Sticky Uses the Nearest Scrolling Mechanism​

A sticky element sticks relative to its nearest relevant ancestor with a scrolling mechanism, commonly created by overflow: auto, scroll, hidden or overlay.

.table-wrapper {
max-block-size: 24rem;
overflow: auto;
}

thead {
position: sticky;
top: 0;
background: white;
}

Here the table header sticks inside .table-wrapper, not necessarily against the browser viewport.

Why Sticky Appears Not to Work​

Check these conditions:

  • Is an inset such as top: 0 set on the correct axis?
  • Which ancestor creates the scrolling mechanism?
  • Is there enough scrollable distance for the element to move?
  • Does the containing block end before the desired sticky range?
  • Is the sticky element as tall as, or stretched to the height of, its container?
  • Is another element painting over it because of stacking order?

Do not automatically remove overflow: hidden; it may be required for clipping. First identify which ancestor should own scrolling.

Stacking and z-index​

z-index Is Contextual​

When positioned elements overlap, z-index helps determine their stacking level within the current stacking context.

.dialog-backdrop {
position: fixed;
inset: 0;
z-index: 10;
}

.dialog {
position: fixed;
z-index: 20;
}

A higher number is not globally above every lower number. Descendants are constrained by the stacking context created by their ancestors.

Root stacking context
├── Header: z-index 10
└── Main: z-index 1
└── Tooltip: z-index 9999

The tooltip can still appear below the header because the entire Main stacking context is below the header.

What Creates a Stacking Context?​

Common causes include:

  • the root element
  • positioned elements with a non-auto z-index
  • position: fixed or sticky
  • opacity below 1
  • transforms and filters
  • isolation: isolate
  • certain containment and container-query values
  • elements placed in the browser's top layer

When z-index seems ineffective, inspect ancestor stacking contexts instead of continually increasing the number.

Practical Patterns​

Badge Inside a Card​

.avatar {
position: relative;
inline-size: 4rem;
block-size: 4rem;
}

.status-dot {
position: absolute;
inset-inline-end: 0;
inset-block-end: 0;
inline-size: 0.875rem;
block-size: 0.875rem;
border: 2px solid white;
border-radius: 50%;
background: green;
}

The avatar stays in flow, while the status indicator is layered inside its bounds.

Fixed Header Without Covered Content​

A fixed header does not reserve layout space. Add intentional spacing so it does not cover the page:

:root {
--header-height: 4rem;
}

.site-header {
position: fixed;
inset: 0 0 auto;
min-block-size: var(--header-height);
}

main {
padding-block-start: var(--header-height);
}

If the header does not need to remain detached from a container, sticky may require less manual compensation.

Responsive and Accessible Positioning​

  • Ensure fixed or sticky UI does not cover focused controls or headings.
  • Test browser zoom, large text and narrow viewports.
  • Preserve logical reading and keyboard order in the DOM.
  • Do not use visual positioning to create a different semantic order.
  • Respect safe-area insets for controls near mobile screen edges.
  • Keep dismiss buttons reachable when overlays are zoomed.
.floating-action {
position: fixed;
inset-inline-end: max(1rem, env(safe-area-inset-right));
inset-block-end: max(1rem, env(safe-area-inset-bottom));
}

Performance Considerations​

Fixed and sticky elements can require frequent repainting or compositing during scroll, especially when they are large or use expensive visual effects.

  • Measure scrolling in browser performance tools.
  • Avoid large shadows or filters when profiling shows costly paint.
  • Do not add will-change to every positioned element.
  • Keep overlay and stacking structures understandable.
  • Prefer transforms for motion when they meet the layout requirement.

Common Mistakes​

MistakeBetter approach
Using absolute positioning for the whole page layoutUse normal flow, Flexbox or Grid for structural layout
Saying absolute always uses its parentIdentify the actual containing block
Forgetting position: relative on an intended anchorEstablish the containing block deliberately
Assuming fixed always uses the viewportCheck ancestors for transforms, filters, perspective or containment
Expecting sticky to work without an insetSet a non-auto inset on the sticky axis
Ignoring ancestor overflow when debugging stickyFind the nearest scrolling mechanism
Treating sticky as removed from flowRemember that it keeps its normal-flow space
Raising z-index indefinitelyInspect the stacking-context hierarchy
Reordering content visually with offsetsKeep DOM order aligned with reading and focus order
Letting fixed UI cover content at zoomTest narrow, zoomed and large-text layouts

Official References​

Summary​

CSS positioning controls whether an element participates in normal flow and which containing block, viewport or scroll boundary determines its final placement.

Should the element keep space in normal flow?
├── Yes
│ ├── No offset needed -> static
│ ├── Offset from itself -> relative
│ └── Constrain during scroll -> sticky
└── No
├── Position inside a containing block -> absolute
└── Position near the viewport -> fixed, usually

Then inspect containing blocks, insets and stacking contexts.