CSS layout reference

Five ways to position a box in CSS.

The position property answers two questions: does the element keep its seat in the normal flow, and what are top, right, bottom, and left measured from? Every value is a different pair of answers.

Try it

The highlighted box is the second of several siblings inside a scrolling frame. Switch its position, change the offset, then scroll the frame.

sibling A
.subject
sibling C
sibling D
sibling E
sibling F
sibling G
sibling H
sibling I
.subject {
  position: sticky;
  top: 24px;
}

Scroll the frame. The box keeps its seat until it would pass the top offset, then pins there. Only the top offset matters for vertical scrolling.

Side by side

ValueKeeps its spaceOffsets measured fromOn scrollz-index works
staticYesNothing — offsets are ignoredMoves with contentNo*
relativeYes, at its original spotIts own normal positionMoves with contentYes
absoluteNoNearest positioned ancestor’s padding boxMoves with that ancestorYes
fixedNoThe viewport (usually)Stays putYes — always a stacking context
stickyYesNearest scrolling ancestor’s edgeMoves, then pins at the thresholdYes — always a stacking context

* Flex and grid items accept z-index even when static.

static

The default. The element sits wherever normal flow, flexbox, or grid puts it. Offsets and (outside flex and grid) z-index do nothing.

Use it for

  • Nearly everything. Flow, flex, and grid should do the layout; positioning is for exceptions.
  • Resetting a positioned element at a breakpoint — a fixed sidebar that becomes an ordinary block on mobile.

Watch for

  • A static parent is not an anchor. An absolute child skips straight past it to the next positioned ancestor.

relative

The element lays out as if static, then is drawn shifted by its offsets. Its original space stays reserved, so neighbours never move. With no offsets it looks identical to static — and that is its most common use.

Use it for

  • Making a parent the anchor for absolute children, such as a card holding a badge.
  • Nudging an icon a pixel or two to sit on the text baseline.
  • Enabling z-index on an element without moving it.

Watch for

  • Large offsets leave a hole where the element used to be and overlap whatever it lands on. For real movement prefer transform: translate() or fix the layout.
.card  { position: relative; }          /* anchor, no offsets */
.badge { position: absolute; top: -8px; right: -8px; }

absolute

Removed from flow — siblings close up as if it were never there — and placed against the padding box of the nearest ancestor whose position is anything but static. With none, it anchors to the page’s initial containing block.

Use it for

  • Notification badges, close buttons in a dialog corner, image caption overlays.
  • Dropdown menus and tooltips hung off a trigger.
  • Full-cover layers: inset: 0 stretches to fill the anchor.
  • Decorative shapes that must not affect layout.

Watch for

  • Forgetting position: relative on the intended parent — the element flies to the top of the page.
  • It contributes no height. A parent holding only absolute children collapses to zero.
  • An ancestor with overflow: hidden clips it; menus inside scroll areas usually need a portal or the popover API.
.overlay { position: absolute; inset: 0; }   /* top/right/bottom/left: 0 */
.center  { position: absolute; inset: 0; margin: auto; width: 200px; height: 80px; }

fixed

Removed from flow and placed against the viewport, so it holds still while the page scrolls underneath. It always creates its own stacking context.

Use it for

  • Modal backdrops and dialogs (inset: 0).
  • Toasts, cookie banners, chat launchers, back-to-top buttons.
  • App shells where a nav bar must be visible from any scroll position.

Watch for

  • An ancestor with transform, filter, perspective, backdrop-filter, contain: paint, or will-change: transform becomes the containing block instead of the viewport. The playground relies on this; in production it is the classic “my modal is trapped inside a card” bug.
  • It covers content. Reserve space with padding or scroll-padding-top so anchors do not land underneath a fixed header.
  • On phones, add env(safe-area-inset-*) to bars at the screen edge, and expect the on-screen keyboard to cover bottom bars.
.toast { position: fixed; right: 16px;
         bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }

sticky

A hybrid: it behaves as relative until its scrolling ancestor would carry it past the offset you set, then it pins there — but only while its parent is still on screen. It keeps its space in flow, so nothing jumps when it pins.

Use it for

  • Site headers that should take up room at the top of the page, unlike fixed.
  • Table header rows and frozen first columns (left: 0).
  • Section headings in long lists — each one replaces the last, like a contacts app.
  • Sidebars and tables of contents that follow the article, then stop at its end.

Watch for

  • No threshold, no stick: you must set top (or another side).
  • Any ancestor with overflow: hidden, auto, or scroll becomes the scroll container it sticks to — often one that never scrolls. Use overflow: clip when you only need clipping.
  • It can only travel within its parent. A sticky element as tall as its parent has nowhere to go.
  • In a flex or grid row it stretches to full height by default; add align-self: start.
thead th { position: sticky; top: 0; background: var(--card); }
.toc     { position: sticky; top: 24px; align-self: start; }

Picking one

It should just sit in the layout
static
It needs to anchor a child, or take a z-index, without moving
relative
It is attached to a specific element and must not take up space
absolute
It is attached to the screen, regardless of scroll
fixed
It belongs to the content but should stay visible while that content is on screen
sticky

Two properties worth knowing alongside these: inset is shorthand for all four offsets, and the logical versions (inset-inline-start, inset-block-end) follow writing direction, so a badge pinned to the “end” corner flips correctly in right-to-left layouts.

Beyond the five values

Three things people reach for position to do, and the tools that now do them better.

place-items: center

Centring a box

Absolute positioning was the centring trick for a decade: top and left at 50%, then a negative translate. It still works, but it takes the box out of flow, so the parent cannot size to it. When the box is ordinary content, grid or flex centres it in one line and keeps it in the layout.

Use it for

  • display: grid; place-items: center on the parent for content that should stay in flow — empty states, hero copy, a spinner in a panel.
  • inset: 0; margin: auto on an absolute box with a set size, for overlays centred on an anchor. No transform, so text stays crisp and the box can still be transformed for animation.
  • The translate(-50%, -50%) version when the box has no fixed size and must overlay something.

Watch for

  • The translate version can land on a half pixel and blur text or borders.
  • It also spends the transform property, so animating scale or rotate on the same element means repeating the translate in every keyframe.
  • Centring with absolute inside a parent that has no height centres on nothing: the parent collapses, because absolute children contribute no size.
.parent { display: grid; place-items: center; }           /* in flow */
.badge  { position: absolute; inset: 0; margin: auto;
          width: 8rem; height: 2rem; }                    /* overlay, sized */
.float  { position: absolute; top: 50%; left: 50%;
          translate: -50% -50%; }                         /* overlay, unsized */

position-anchor

Anchor positioning

An absolute box can only measure from its containing block, which is why tooltips and menus have needed JavaScript to follow a trigger that lives somewhere else in the DOM. Anchor positioning removes that limit: name any element as an anchor, and another element can place itself against its edges — and flip to the other side when it would run off-screen.

Use it for

  • Tooltips, dropdown menus, and popovers tethered to their trigger with no positioning library.
  • Pairing with the popover attribute: the top layer escapes clipping, the anchor supplies the position.
  • position-try-fallbacks: flip-block to open upward when there is no room below.

Watch for

  • It is newer than everything else on this page and support is still arriving. Wrap it in @supports (anchor-name: --a) and keep a plain fallback position or a JavaScript positioner.
  • The positioned element must be absolute or fixed, and anchor names are scoped: a name inside a contained or shadow tree is not visible outside it.
  • A repeated anchor name resolves to the last matching element in the tree. For lists of triggers, set the name inline per item or scope it with anchor-scope.
.trigger { anchor-name: --menu; }

@supports (anchor-name: --a) {
  .menu { position: absolute; position-anchor: --menu;
          top: anchor(bottom); left: anchor(left);
          position-try-fallbacks: flip-block; }
}

popover

The top layer

A modal <dialog> and any element with the popover attribute are lifted into the top layer: a separate plane the browser paints above the whole document. Nothing in the page can sit over it, no z-index applies to it, and no ancestor’s overflow or transform can clip or trap it — the two classic failures of absolute and fixed overlays.

Use it for

  • Modals: dialog.showModal() also makes the rest of the page inert, traps focus, and closes on Escape.
  • Menus, pickers, and toasts with the popover attribute — light dismiss and keyboard handling included, no JavaScript needed to open one.
  • Replacing a portal to document.body. The element stays where it belongs in the DOM and in the component tree.

Watch for

  • Top-layer elements are positioned against the viewport and centred by default; a menu needs anchor positioning or script to sit beside its trigger.
  • Order within the top layer is the order things were opened. z-index does nothing, so a toast opened before a dialog is underneath it.
  • dialog.show() — without Modal — does not use the top layer and is an ordinary positioned box.
  • A modal dialog makes the page inert but does not stop it scrolling; that still needs a scroll lock.
<button popovertarget="filters">Filters</button>
<div id="filters" popover>…</div>

dialog::backdrop { background: rgb(0 0 0 / 0.5); }

Keep going

Position decides where. Stacking decides what is on top.

The stacking reference explains why z-index: 9999 still loses, what creates a stacking context, and how isolation fixes it.

Open the stacking reference

We'd like to use Google cookies to understand how Nodlume is used and to measure our advertising. Nothing loads until you choose, and declining does not affect anything in the app.