Positioning, stacking and overflow

Static through fixed, the containing block, sticky behaviour, z-index and stacking contexts, scroll containers and aspect ratio.

The five positioning modes

positionLeft in normal flow?Resolves against
staticYesNothing — offsets are ignored
relativeYes, space is preservedIts own original position
absoluteNo, removed from flowThe nearest positioned ancestor, else the initial containing block
fixedNoThe viewport, unless an ancestor creates a containing block
stickyYesIts scrollport, once the inset threshold is crossed
.card { position: relative; }        /* becomes the containing block */

.badge {
  position: absolute;
  inset-block-start: 8px;   /* logical top */
  inset-inline-end: 8px;    /* logical right */
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
}

Sticky elements need an inset threshold such as top: 0 to have a position to stick at, and they only travel within their own parent. If the parent is shorter than the scroll distance, the element stops when the parent ends.

⚠️
A position: sticky element silently stops working if any ancestor sets overflow: hidden, auto or scroll. That ancestor becomes the element's scrollport, and it never scrolls — remove the overflow from the ancestor instead of adding more positioning.

z-index and stacking contexts

z-index only applies to positioned elements or to flex and grid children. More importantly, it only competes with siblings inside the same stacking context: a large number cannot escape its parent's arena.

.modal  { position: fixed; z-index: 100; }
.tooltip { position: absolute; z-index: 200; }  /* still behind the modal if trapped */

.card { isolation: isolate; }   /* deliberate, explicit stacking context */
  • A stacking context is created by positioning plus a non-auto z-index, opacity below 1, a transform, filter or backdrop-filter, will-change, contain: paint, isolation: isolate, and flex or grid children with a z-index.
  • Children of a stacking context are painted as one unit: no descendant can rise above a sibling of its ancestor.
  • Within one context, painting order runs background, then negative z-index, then in-flow blocks, then floats, then inline content, then z-index zero and above.
  • Replace the z-index arms race with a small named scale, and document it next to the layer declaration.

Overflow, scroll containers and aspect ratio

overflow decides what happens when content is larger than its box. Making a box scroll also makes it a scroll container, which changes how it clips, how it behaves with sticky children, and whether it can scroll-chain into the page behind it.

.scroller {
  overflow: auto;
  max-block-size: 60vh;
  overscroll-behavior: contain;  /* do not chain scrolling to the page */
}

.thumb {
  inline-size: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;             /* crop instead of distort */
}
  • overflow on one axis forces the other away from visible, which is a common source of surprise.
  • aspect-ratio reserves height from a width, so media never shifts the layout while loading.
  • object-fit: cover fills the box without distortion; contain letterboxes instead.
  • Give scrollable regions keyboard focus so they can be scrolled without a mouse.
  • Prefer overflow: clip when you want clipping without creating a scroll container.

FAQ

Why does my z-index have no effect?
Either the element is still static, or an ancestor created a stacking context and trapped it. Find the ancestor with a transform, filter or opacity value and reconsider which one needs its own context.
What is the difference between absolute and fixed?
Fixed resolves against the viewport, so it survives scrolling. But an ancestor with a transform, filter or perspective becomes its containing block, which makes a fixed element scroll with that ancestor instead.

Modern selectors and cascade control Debugging CSS, resets and browser support

Last refreshed 2026-09-18.