← Back to the library
FrontendHow it works · Core · 14 min

CSS layout: flow, formatting contexts, flex, grid and stacking

How does the browser decide where every box goes and what paints on top, and which layout tool fits which job?

Every box is laid out by the formatting context of its parent: normal flow, flex, grid or positioning. Pick flexbox for one-dimensional distribution, grid for two-dimensional structure, positioning for overlays, and reason about z-index only within the stacking context it belongs to.

THE MENTAL MODEL

Layout is a tree of formatting contexts, not a page-wide coordinate system. Each container decides how its children are sized and placed: block flow stacks them vertically with collapsing margins, inline flow wraps them into lines, flex distributes them along one axis from their content outward, and grid places them into tracks defined from the container inward. Positioned elements leave flow and are placed relative to a containing block. Painting is a separate tree: stacking contexts are atomic layers, and z-index only orders siblings inside the same context, which is why a z-index of 9999 can still lose. Modern additions make components self-contained: container queries respond to the space a component gets, logical properties follow writing direction, subgrid lets nested items share the parent's tracks, and anchor positioning attaches overlays to other elements without JavaScript.

HOW IT FITS TOGETHER

Flexbox vs grid vs positioningMost real layouts nest them: grid for the page and card structure, flex inside rows and toolbars, positioning or the top layer for overlays.
DimensionsSizing driven byStrong atWeak at
Normal flowBlock: vertical; inline: linesContent and width of containerDocuments, text, accessible reading orderSide-by-side layouts, equal heights
FlexboxOne axis (row or column)Item content, then grow/shrinkToolbars, nav, centering, wrapping chipsAligning items across separate rows
GridTwo axes (rows and columns)Tracks defined on the containerPage shells, card grids, forms, overlapContent-sized wrapping sequences
SubgridInherits parent tracksParent grid's track sizesAligning card headers and footersCreating implicit tracks of its own
position: absolute/fixedOut of flow, inset-basedContaining block (nearest positioned)Badges, overlays relative to a boxAnything that must push siblings
Anchor positioningOut of flow, tied to an anchorAnchor's edges via anchor()Tooltips, menus, popovers with flipOlder browsers (Baseline only since 2026)
How a stacking context decides what is on topz-index competes only among siblings in the same stacking context. Fix layering by fixing which context an element lives in.
  1. Property creates a contextRoot element; positioned with z-index not auto; fixed or sticky; flex/grid item with z-index; opacity < 1; transform, filter, clip-path, mask; isolation: isolate; contain: paint; container-type size or inline-size.
  2. Context becomes one layerEverything inside is painted together and flattened into the parent context as a single unit at the context's own z-index.
  3. Children ordered insideWithin the context: background and borders, negative z-index, in-flow blocks, floats, inline content, z-index auto/0, then positive z-index.
  4. Ties broken by DOM orderEqual z-index in the same context paints in source order, later on top.
  5. Nested z-index is cappedA child with z-index: 9999 inside a context at z-index: 1 still paints below a sibling context at z-index: 2.
  6. Top layer sits above allModal dialog, popover and fullscreen elements render in the top layer, outside every stacking context, with their ::backdrop.

KEY TERMS

Box model and box-sizing
Each box has content, padding, border and margin. With the default content-box, width excludes padding and border, so width: 100% plus padding overflows; box-sizing: border-box makes width include them. Vertical margins between blocks in normal flow collapse to the larger of the two; flex and grid items never collapse margins.
Formatting contexts
A block formatting context contains its floats, excludes outside floats and stops child margins collapsing through it. display: flow-root creates one without the side effects of overflow: hidden. Flex and grid containers create flex and grid formatting contexts; their children become items and stop participating in block flow.
Flex vs grid sizing direction
Flexbox is content-out: items start at flex-basis or content size and grow or shrink to share leftover space along one axis. Grid is layout-in: the container defines tracks (fr, minmax(), auto-fit/auto-fill) and items are placed into them on two axes. Note that min-width: auto on flex items prevents them shrinking below their content; set min-width: 0 for truncation.
Positioning and containing blocks
relative offsets a box but keeps its space; absolute removes it from flow and positions against the nearest positioned ancestor (or one with a transform, filter or contain); fixed uses the viewport unless an ancestor has a transform; sticky behaves as relative until a scroll threshold, within its scroll container.
Stacking contexts and the top layer
z-index only works on positioned elements and flex or grid items, and only orders elements within the same stacking context. Many unrelated properties (opacity, transform, filter, will-change, container-type) create a context, which is why a harmless animation can break a dropdown. isolation: isolate creates one deliberately; dialog and popover escape all of them through the top layer.
Container queries and logical properties
container-type: inline-size makes an element a query container, and @container (width > 30rem) styles descendants by the space they actually get, with cqi units relative to it. Size containment means the container cannot be sized by its own contents on that axis. Logical properties (margin-inline, padding-block, inset-inline-start, inline-size) follow writing mode and direction, so RTL and vertical text work without overrides.
Subgrid and anchor positioning
grid-template-columns or rows: subgrid lets a nested grid reuse its parent's tracks, line names and gaps; it has worked in all major browsers since September 2023 (now Baseline widely available) but cannot create implicit tracks in a subgridded axis. CSS anchor positioning (anchor-name, position-anchor, position-area, anchor(), position-try-fallbacks) became Baseline newly available in January 2026 with Firefox 147; position-visibility and anchor-size() are less interoperable.

IN YOUR STACK

CSS · CSS Grid Client

/* <body class="page"><header/><nav/><main/><aside/><footer/></body> */
.page {
  display: grid;
  min-block-size: 100dvh;
  grid-template:
    "header" auto
    "main"   1fr
    "nav"    auto
    "aside"  auto
    "footer" auto / 1fr;
  gap: 1rem;
}
@media (width >= 60rem) {
  .page {
    grid-template:
      "header header header" auto
      "nav    main   aside"  1fr
      "footer footer footer" auto
      / 14rem minmax(0, 1fr) 16rem;
  }
}
.page > header { grid-area: header; }  .page > nav    { grid-area: nav; }
.page > main   { grid-area: main; }    .page > aside  { grid-area: aside; }
.page > footer { grid-area: footer; }
  • minmax(0, 1fr) instead of 1fr lets the main column shrink below its longest unbreakable content (code blocks, tables) instead of overflowing.
  • grid-template-areas changes only the visual order. Keep the DOM in a sensible reading and tab order; a large mismatch fails WCAG meaningful sequence.
  • 100dvh tracks the dynamic viewport on mobile, where 100vh includes the area behind retractable toolbars.
CSS Grid documentation (opens in new tab)

CSS · Flexbox Client

/* <nav class="nav"><a class="logo"/><ul class="links">...</ul><a class="cta"/></nav> */
.nav {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding-inline: 1rem;        /* logical: flips correctly in RTL */
}
.nav .links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  margin: 0;
  padding: 0;
  list-style: none;
  min-width: 0;                /* allow shrinking below content size */
}
.nav .cta {
  margin-inline-start: auto;   /* pushes the button to the end */
  flex-shrink: 0;
}
.nav a:focus-visible {
  outline: 3px solid currentColor;
  outline-offset: 2px;
}
  • An auto margin in a flex container absorbs free space, which is the simplest way to split a row into start and end groups.
  • list-style: none on a ul makes Safari/VoiceOver drop list semantics, except when the list is inside a nav element; elsewhere add role="list" if the count matters.
Flexbox documentation (opens in new tab)

CSS · Container queries Client

/* <div class="card-slot"><article class="card"><img/><div class="body"/></article></div> */
.card-slot {
  container: card / inline-size;
}
.card {
  display: grid;
  gap: 0.75rem;
  padding: 1rem;
}
.card img {
  inline-size: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}
@container card (width >= 32rem) {
  .card {
    grid-template-columns: 12rem 1fr;
    align-items: start;
  }
  .card img { aspect-ratio: 1; }
  .card h2 { font-size: clamp(1.25rem, 1rem + 2cqi, 1.75rem); }
}
  • Query a wrapper, not the card itself: an element cannot style itself based on its own container size, and inline-size containment stops it sizing from its content.
  • The same card now adapts in a sidebar, a grid cell or a full-width row with no viewport media queries.
  • container-type also creates a stacking context and a formatting context; check overlays that live inside it.
Container queries documentation (opens in new tab)

CSS · Stacking contexts Client

/* Bug: the dropdown (z-index: 1000) renders under the sticky header. */
.card {
  transform: translateY(0);   /* hover animation: creates a stacking context */
}
.card .dropdown {
  position: absolute;
  z-index: 1000;              /* only competes inside .card's context */
}
.header {
  position: sticky;           /* sticky creates its own context */
  top: 0;
  z-index: 10;                /* beats .card (z-index auto => level 0) */
}

/* Fix 1: give the component its own, deliberate context and level. */
.card { position: relative; isolation: isolate; z-index: 0; }
.card:has(.dropdown[data-open]) { z-index: 20; }

/* Fix 2 (preferred for menus): render the menu in the top layer.
   <div popover id="menu">...</div> escapes every stacking context
   and every overflow: hidden ancestor. */
  • Raising z-index higher never fixes a stacking-context bug; find the ancestor that creates the context (DevTools layers panels show them).
  • Use a small token scale (base, dropdown, sticky, overlay) per context instead of ever-larger numbers.
  • The popover attribute and modal dialogs use the top layer, which also escapes overflow clipping.
Stacking contexts documentation (opens in new tab)

CSS · CSS Grid subgrid Client

/* <ul class="cards"><li class="card"><h3/><p/><a/></li>...</ul> */
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: 1.5rem;
}
.card {
  display: grid;
  grid-row: span 3;               /* the card occupies three parent rows */
  grid-template-rows: subgrid;    /* ...and shares their heights */
  row-gap: 0.5rem;                /* overrides the inherited 1.5rem gap */
  padding: 1rem;
  border: 1px solid;
}
.card a {
  align-self: end;                /* every link sits on the same line */
}
/* Titles, bodies and links now line up across every card in a row,
   even when one title wraps to three lines. */
  • Works in all major browsers since September 2023 and is now Baseline widely available.
  • The subgridded axis cannot create implicit tracks, so the span must match the number of children.
  • Line names and gaps are inherited from the parent; line numbers restart at 1 inside the subgrid.
CSS Grid subgrid documentation (opens in new tab)

HTML + CSS · Anchor positioning + popover Client

<button class="trigger" popovertarget="account-menu">Account</button>
<div id="account-menu" popover>
  <a href="/settings">Settings</a>
  <a href="/logout">Sign out</a>
</div>

<style>
  .trigger { anchor-name: --account; }

  #account-menu {
    position-anchor: --account;
    position-area: block-end span-inline-end; /* below, aligned to start */
    position-try-fallbacks: flip-block, flip-inline;
    margin: 0.25rem 0 0;
    inset: auto;            /* reset popover's default centering */
  }

  @supports not (anchor-name: --a) {
    #account-menu { inset: 4rem 1rem auto auto; } /* fixed fallback */
  }
</style>
  • Baseline newly available since January 2026 (Chrome/Edge 125, Safari 26, Firefox 147): add a fallback or a positioning library for older browsers still in your support matrix.
  • Early Chrome versions used inset-area, renamed to position-area; position-visibility and anchor-size() still have interop gaps.
  • popover gives top-layer rendering, light dismiss, Escape to close and focus return for free; anchor positioning only handles placement.
Anchor positioning + popover documentation (opens in new tab)

WHEN TO REACH FOR WHAT

Flexbox

Use when Items flow along one axis and their content should decide their size: nav bars, button groups, tag lists, centering.

Avoid when Items in different rows must align in columns; use grid.

Grid

Use when You define the structure first: page shells, card galleries, forms with label/field columns, overlapping layers in one cell.

Avoid when A simple row of unknown-width items that should just wrap.

Subgrid

Use when Nested components (cards, form rows) must align their internals with siblings across a parent grid.

Avoid when The child needs a variable number of auto-generated tracks in that axis.

Absolute or fixed positioning

Use when Decorations and overlays relative to a known box: badges, close buttons, sticky headers.

Avoid when Menus and dialogs that must escape overflow and stacking; use popover or dialog in the top layer.

Container queries

Use when Reusable components placed in containers of varying width.

Avoid when Page-level changes driven by the viewport or user preferences; use media queries.

WHERE IT BITES

  • Adding z-index: 9999 instead of finding the ancestor whose transform, opacity, filter or container-type created a stacking context.
  • Flex or grid items refusing to shrink and overflowing (long URLs, code, tables): flex items and 1fr tracks default to a min-content minimum. Use min-width: 0 or minmax(0, 1fr).
  • Reordering visually with order, grid-area or row-reverse so the tab and screen reader order no longer match what users see.
  • Using overflow: hidden to clear floats or contain margins, then clipping focus rings, shadows and dropdowns; display: flow-root does the job without clipping.
  • Putting container-type on the element you want to restyle, or on an element whose width comes from its content, which then collapses to zero.
  • Shipping anchor positioning without a fallback for browsers outside the 2026 Baseline window.

CLOSE THE AI. EXPLAIN THIS.

A dropdown inside a card with a hover transform appears behind the next card and behind the sticky header, despite z-index: 1000. Explain which stacking contexts exist, why the number does not matter, and two different fixes.

SOURCES

Explainer reviewed