Concepts — Layout: Flex, Grid, Intrinsic Sizing & Container Queries

Phase 1 · Platform substrate · Specification area §4 (flexbox, grid, subgrid, container queries, media queries, intrinsic sizing, min/max-content, stacking contexts, positioning). Depends on fe-05.


0. Orientation

The 60-second version

Layout is the pipeline stage that turns styled boxes into geometry. It is also the stage carrying the most durable folklore in frontend engineering, and this module's job is to replace that folklore with numbers.

The headline: six layout modes over 20,000 items span 1.4× — and the two modes most often recommended for performance, absolute positioning and float, were the two slowest. Flex and grid were within noise of block.

Put that next to the previous module's result and the picture resolves:

DecisionEffect on layout
Choosing the "fastest" layout mode1.4×
content-visibility: auto (fe-05)98% reduction
Avoiding forced synchronous layout (fe-01)98× on a thrashing loop

Layout mode is a rounding error. Layout scope is the decision.

So layout mode should be chosen for expressiveness — which algorithm states your intent with the fewest workarounds — and the performance attention belongs two modules over.

The other half: a capability, not a speed

The second half of this module is not about performance at all. Container queries are the first mechanism that lets a component respond to its own context rather than the viewport. Measured: a @container rule responded when its container narrowed to 400 px while the viewport stayed at 1200 px; the equivalent @media rule did nothing.

That is a capability difference. A component styled with media queries produces a main-column layout when dropped into a sidebar, and there is no way to fix that from inside the component. Container queries are what make genuinely reusable components possible.

What you should be able to do after this module

CapabilityEvidence you have it
Refuse a layout-mode performance refactorCite 1.4× against fe-05's 98% and fe-01's 98×
Choose a mode for the right reasonPick grid/flex/block from the relationship being expressed
Debug "my flex item won't shrink"Name min-width: auto → min-content without looking it up
Deploy container queries correctlyPlace container-type on a wrapper with externally-determined width
Know when @media is still rightSeparate page-shell composition from component internals

1. What is it

Layout is the pipeline stage that turns styled boxes into geometry. This module covers the algorithms you choose between (block, flex, grid, absolute), the sizing vocabulary that decides how much measuring the browser must do, and container queries — the first mechanism that lets a component respond to its own context rather than the viewport.

The mental frame: layout is a constraint-solving pass

Every layout algorithm answers the same two questions for every box — how big? and where? — but they differ in what information they need and in which direction that information flows.

   PARENT constrains child        (available width flows DOWN)
        │
        ▼
   CHILD reports intrinsic size   (content size flows UP)
        │
        ▼
   PARENT resolves final geometry

That two-way flow is the source of most layout cost and most layout confusion:

  • Downward-only sizing is cheap. width: 200px needs nothing from the content.
  • Upward-flowing sizing requires measurement. min-content must measure every unbreakable unit in the box before the parent can decide anything.
  • Circular dependencies are forbidden, and the rules preventing them are why container-type: inline-size must imply size containment (§3.4) — a container cannot size itself from content that is styled based on the container's size.

Hold that frame and the measured numbers stop being trivia: min-content is the dearest keyword because it forces the most upward measurement, and 20%/auto are dearest on relayout because they depend on a parent that just changed.


2. Why it matters

Layout-mode folklore is wrong, and expensive. Measured: block, inline-block, float, flex, grid and absolute positioning across 20,000 items span 1.4× — and absolute positioning, the mode usually described as fastest, was the slowest (75.5 ms vs block's 55.1 ms). On relayout, flex was the fastest (7.3 ms). Teams contort component APIs to avoid flex/grid on performance grounds that do not survive measurement.

Where layout cost actually lives is how much you ask the browser to lay out. Compare this module's 1.4× spread against fe-05's content-visibility result: 98% off initial layout, same DOM. Layout mode is a rounding error; layout scope is the decision.

Container queries change what a component can be. Measured: a @container rule responded when its container narrowed to 400 px while the viewport stayed at 1200 px; the equivalent @media rule did nothing. That is not a performance difference, it is a capability difference — and it is what makes genuinely reusable components possible (fe-38).

The specific waste this module prevents

The layout-mode myth is unusually costly because it does not produce a slow app — it produces a bad API. The sequence:

  1. Someone hears flex/grid are slow.
  2. Components are built with wrapper divs, fixed heights, magic-number margins and absolute positioning to avoid them.
  3. Those workarounds break under long strings, RTL, large text settings, and dynamic content.
  4. Each break is patched with another magic number.

The result is a component nobody wants to change, justified by a 1.4× that was never real. A layout needing no wrapper divs and no magic numbers outperforms a "faster" one that needs both — including on the actual measurements, because the workaround version has more boxes.


3. How it works

+==================================================================+
|  SIZING: how much must the browser MEASURE?                       |
+==================================================================+

  width: 200px        -> no measurement            (cheapest)
  width: 20%          -> depends on parent          (one pass)
  width: auto          -> depends on context
  width: max-content   -> measure content, no wrap
  width: fit-content   -> min(max-content, available)
  width: min-content   -> measure longest unbreakable unit  (dearest: 52.1ms vs 33.1ms)

+==================================================================+
|  RESPONSIVE: what is the rule allowed to ask about?               |
+==================================================================+

  @media     -> the VIEWPORT.  A component cannot know it is in a sidebar.
  @container -> the nearest ancestor with container-type.
                Requires containment on that ancestor -- which is the cost.

     .wrap { container-type: inline-size; }   <- promises size containment
     @container (min-width: 700px) { ... }

3.1 Layout modes, measured

20,000 items, identical content, identical visual result:

modeinitial layoutinitial stylerelayout (width change)
block55.1 ms11.7 ms23.5 ms
inline-block55.7 ms11.3 ms9.5 ms
flex59.3 ms11.3 ms7.3 ms
grid62.5 ms10.5 ms14.7 ms
float75.2 ms11.0 ms36.2 ms
absolute75.5 ms27.7 ms9.8 ms

Spread: 1.4×. Three results contradict common advice:

  • Absolute positioning was slowest, and carried 2.4× the style cost of every other mode.
  • Flex was fastest on relayout (7.3 ms), beating block (23.5 ms) by 3.2×.
  • Float was second-slowest on both measures — and it is the mode most often retained for compatibility reasons.

3.2 Why absolute positioning measured slowest

Worth separating into two contributions, because the reasoning generalises:

1. Style cost, 27.7 ms vs ~11 ms. Every item carried inline left/top. Inline styles bypass selector matching but still require per-element value computation, and they defeat the shared computed-style caching that identical elements otherwise benefit from. This is a real property of how absolute layouts are actually built, not a harness artefact.

2. Layout is not avoided. Out-of-flow boxes still need their containing block resolved, their own sizing, and their own paint order. "Out of flow" means "does not affect siblings" — which helps invalidation scope, visible in the relayout column (9.8 ms, better than block's 23.5 ms), not initial cost.

The generalisable lesson: a technique can be worse on one axis and better on another, and folklore usually remembers only one of them. Absolute positioning genuinely relayouts well. It was sold as laying out fast, which is a different claim, and false.

3.3 Intrinsic sizing

8,000 items:

widthinitial layoutrelayout
200px33.1 ms1.5 ms
20%31.9 ms14.6 ms
min-content52.1 ms2.1 ms
max-content35.4 ms1.4 ms
fit-content35.2 ms1.7 ms
auto33.9 ms9.5 ms

Spread: 1.6×. Only min-content is notably expensive — the browser must measure the longest unbreakable unit in every box. fit-content and max-content land within ~7% of a fixed width.

The relayout column inverts the picture, and this is the more interesting half: 20% and auto are dearest to re-lay-out (14.6 ms, 9.5 ms) because they depend on the parent, while the intrinsic keywords are nearly free once measured. Percentage sizing looks cheap and is expensive to update; intrinsic sizing looks expensive and is cheap to update. Which matters depends on whether your layout changes.

The idiom that explains half of all flexbox bugs:

.item { min-width: 0; }            /* opts OUT of the default min-content floor */
.item { min-width: min-content; }  /* opts back in explicitly */

Flex and grid items default to min-width: auto, which resolves to min-content. That is the single most common cause of "my flex item won't shrink" and of unexpected overflow in grid. Setting min-width: 0 is the fix — and it is also a small performance win, because it removes a measurement.

3.4 Container queries — capability, cost, constraint

strategy (8,000 items)initial layoutinitial stylerelayout
none31.8 ms4.3 ms9.1 ms
@media31.8 ms4.8 ms9.7 ms
@container33.2 ms7.3 ms9.3 ms

Cost: +1.4 ms layout, +3.0 ms style across 8,000 elements. Negligible at any realistic component count.

The behavioural result — the reason they exist. Container narrowed to 400 px, viewport unchanged at 1200 px:

padding beforepadding after
@container (min-width: 700px)8px0px — responded
@media (min-width: 700px)8px8px — ignored

The constraint is the real cost. container-type: inline-size implies size containment on the inline axis: the container's inline size may not depend on its contents. Consequences:

  • A container whose width is determined by its content (a fit-content wrapper, a table cell, an inline-block sized to text) cannot be a query container without changing its sizing behaviour.
  • container-type: size (both axes) is stricter still and will collapse height unless one is set.
  • Query containers cannot query themselves. Styles inside @container apply to descendants; a container cannot restyle its own width based on its own width. That circularity is exactly why the containment requirement exists — it is the layout engine forbidding a cycle, not an arbitrary restriction.

Practical rule: put container-type on a wrapper you control that has an externally-determined width, and query it from the component inside. Do not put it on the component root and then try to size that root from its contents.


4. Core terminology

TermDefinition
Formatting contextThe layout algorithm governing a box's children (block, inline, flex, grid)
Intrinsic sizingSizing from content: min-content, max-content, fit-content
min-contentWidth of the largest unbreakable unit; requires measuring every word
max-contentWidth the content would take with no wrapping
fit-contentmin(max-content, available) — usually the keyword you want
min-width: autoThe flex/grid default; resolves to min-content and blocks shrinking
Containing blockThe ancestor box a percentage or absolute offset resolves against
Out of flowAbsolute/fixed positioning: does not affect siblings — an invalidation property, not a speed one
Stacking contextIsolated z-ordering scope; created by transform, opacity<1, filter, will-change, contain: paint
Container query@container; matches against an ancestor with container-type
container-type: inline-sizeEnables inline-axis container queries; implies size containment on that axis
Container query unitcqw, cqh, cqi, cqb — relative to the query container
SubgridChild grid inheriting its parent's tracks, so nested content can align across items
aspect-ratioReserves space before content loads; a CLS tool (fe-11)
GapSpacing that does not participate in the box model; works in flex, grid and multicol
Logical propertyFlow-relative sizing/spacing (inline-size, margin-inline); survives RTL (fe-26)

5. Mental models

Choose layout mode for expressiveness, not speed. The 1.4× spread means the question is which algorithm expresses your intent with the fewest workarounds. Grid for two-dimensional relationships, flex for one-dimensional distribution, block for flow. A layout that needs no wrapper divs and no magic numbers will outperform a "faster" one that needs both.

Sizing keywords are measurement requests. min-content costs 1.6× a fixed width because the browser must measure every unbreakable unit. That is a real cost and usually the right trade — fit-content and max-content measured close to fixed, and they remove entire classes of magic number.

Size flows two ways, and cost follows the upward flow. Constraints flow down from the parent; intrinsic sizes flow up from the content. Anything requiring the upward pass costs measurement, and anything creating a cycle is forbidden — which is the whole explanation for the container-type containment requirement.

Container queries move the responsive boundary from the page to the component. A component that queries the viewport is not reusable: dropping it in a sidebar produces a layout designed for a main column. Container queries make "responsive" a property of the component rather than of the page, which is what a design system needs to be honest.

Containment is the price of a container query. container-type: inline-size implies size containment on that axis, so the container's inline size may no longer depend on its contents. Measured at +1.4 ms layout and +3.0 ms style over 8,000 elements — cheap, but not free, and the constraint matters more than the cost.

A technique is rarely good or bad on one axis. Absolute positioning was slowest to lay out and second-best to relayout. Percentage widths were cheap initially and dearest to update. Folklore compresses these into a single verdict, and the compression is where it goes wrong.


6. Applied: the decisions this module actually makes

6.1 Choosing a layout mode

Since the measured spread is 1.4×, choose for expressiveness:

NeedModeWhy
Two-dimensional relationships, alignment across rows and columnsgridthe only mode that expresses both axes; subgrid extends it to children
One-dimensional distribution, unknown item countflexgap, flex-wrap, and content-based sizing without magic numbers
Document flow, proseblockcheapest, and what the content actually is
Overlay, tooltip, precise placement relative to a containing blockabsolutebut measure — slowest here, with 2.4× the style cost
Legacy compatibility onlyfloatslowest on both axes; no reason to choose it in new code

The performance-shaped question actually worth asking is not "which mode" but "how many boxes am I asking the browser to lay out, and how often?" That is fe-05's territory (content-visibility, containment) and fe-01's (avoiding forced synchronous layout).

6.2 When media queries remain correct

Container queries are not a replacement:

DecisionQuery
How many columns in the page shell@media
Whether the primary nav collapses@media
Print styles@media print
prefers-reduced-motion, prefers-color-scheme, forced-colors@media (user preference, not size)
Whether a card shows a horizontal or stacked layout@container
Whether a data table becomes a definition list@container

The distinction: page-level composition is a viewport question; component internals are a container question. A codebase that uses only media queries cannot build reusable components; one that uses only container queries cannot express its page shell.

6.3 Deploying container queries in a design system

  1. Declare the container on a wrapper, not the component root. The root usually needs to size from context; the wrapper's width is set by the page.
  2. Decide who owns container-type. If the component declares its own, it always works but adds a containment constraint the consumer did not ask for. If the consumer declares it, the component is more flexible and silently degrades when they forget.
  3. Document container query units loudly. cqw inside a component makes it depend on being inside a query container. If a consumer forgets container-type, units resolve against the nearest ancestor that is one — or the small-viewport fallback — silently. This is the failure mode with no error message.
  4. Name your containers. container-name plus @container card (…) prevents a nested container from accidentally becoming the query target.

6.4 Debugging layout — the ordered checklist

  1. "My flex item won't shrink." min-width: auto → min-content. Set min-width: 0. Overwhelmingly the answer; check it before anything else.
  2. "My grid overflows." Same cause, on min-width/min-height of grid items.
  3. "z-index stopped working." An ancestor became a stacking context — transform, opacity < 1, filter, will-change, or contain: paint added for performance (fe-05).
  4. "The container query does nothing." Either no ancestor has container-type, or the element you are trying to restyle is the container (containers cannot query themselves).
  5. "It broke in RTL / with long strings / at large text sizes." A magic number or a physical property. This is the cost of the workarounds adopted to avoid flex/grid.
  6. "Layout is slow." Wrong module. Go to fe-05 for scope and fe-01 for forced synchronous layout — mode is worth 1.4×.

6.5 Reviewing a diff — the five smells

  1. A performance justification for a layout mode. 1.4×. Ask for the measurement and the comparison against containment.
  2. Fixed heights used to align nested content across cards. That is subgrid's job; fixed heights break under long strings and large text settings (fe-26).
  3. cqw/cqi in a shared component with no declared container. Silent fallback.
  4. New code using margin-left/padding-right/text-align: left. Logical properties cost nothing now and a rewrite later.
  5. Media queries inside a reusable component. It is not reusable. The component cannot know it is in a sidebar.

6.6 What breaks at scale

  • Subgrid support assumptions. subgrid is the correct tool for aligning nested content across cards, and its absence is usually worked around with fixed heights that break under long strings or large text settings (fe-26). Check support before designing around it.
  • aspect-ratio and CLS. Reserving space for media before it loads is the cheapest CLS fix available, and it is a layout decision made here rather than a performance patch later (fe-11).
  • Container query units in shared components. Silent resolution against the wrong ancestor. Design-system components should either declare their own container or document the requirement loudly.
  • Stacking contexts created accidentally. The commonest version is a performance change (will-change, contain: paint) breaking an overlay (fe-05).
  • Layout thrash is orthogonal to all of this. fe-01 measured 98× from interleaved reads and writes. No layout mode choice recovers that.

7. Common misconceptions

  1. "Flex and grid are slow; use block or absolute." Measured backwards. Absolute was slowest overall (75.5 ms) with the highest style cost, and flex was fastest on relayout (7.3 ms).

  2. "Absolute positioning avoids layout." It takes the element out of flow; it does not avoid layout. It measured worst here, partly because per-element inline positioning inflates style cost (27.7 ms vs ~11 ms). "Out of flow" is an invalidation property, not a speed one.

  3. "Intrinsic sizing is too expensive." 1.6× worst case, and only min-content is notably dearer. fit-content and max-content measured within ~7% of a fixed width.

  4. "Container queries are expensive." +1.4 ms of layout across 8,000 elements. The real cost is the containment constraint, not the query.

  5. "Container queries replace media queries." They answer different questions. Page-level decisions — how many columns in the shell, whether a nav collapses — are viewport questions. Component-level decisions are container questions.

  6. "Layout performance is about picking the right properties." fe-05 measured 98% off initial layout from content-visibility. This module measured 1.4× across every layout mode. Scope dominates mode by two orders of magnitude.

  7. "Percentage widths are cheap." Cheap to lay out initially (31.9 ms), dearest to relayout (14.6 ms) because they depend on a parent that just changed. The intrinsic keywords invert that.

  8. "A container query container can style itself." It cannot — that would be circular, which is precisely why container-type implies size containment. Query containers style their descendants.

  9. "min-width: 0 is a hack." It is opting out of the flex/grid default of min-width: auto, which resolves to min-content. It is the documented mechanism, and it removes a measurement.


8. Interview talking points

  • "We measured six layout modes over 20,000 items and the spread was 1.4×, with absolute positioning slowest and flex fastest on relayout. Choosing layout mode for performance is optimising the wrong variable — layout scope is worth ~98%, mode is worth ~40%."
  • "The cost of the layout-mode myth isn't a slow app, it's a bad API. Teams add wrapper divs, fixed heights and magic numbers to avoid flex and grid, and those break under long strings, RTL and large text settings. The workaround version usually has more boxes, so it's slower too."
  • "Absolute positioning was slowest to lay out and second-best to relayout, because out-of-flow means 'doesn't affect siblings' — that's an invalidation property, not a speed one. Folklore compresses two axes into one verdict and that's where it goes wrong."
  • "Container queries are a capability change, not a performance one. A component that queries the viewport can't be reused in a sidebar. We verified it: narrowing the container to 400px changed @container styles and left @media untouched at a 1200px viewport."
  • "The cost of a container query is container-type, which implies size containment. That's a constraint on your layout, not a millisecond figure — and it exists because a container can't size itself from content it's styling based on its own size. It's the engine forbidding a cycle."
  • "min-content is the one intrinsic keyword with a real cost, because the browser measures every unbreakable unit. fit-content is nearly free and removes most magic numbers. And the flex/grid default of min-width: auto resolving to min-content is behind most 'my item won't shrink' bugs."

9. Connections to other modules

  • fe-05 — direct comparison: containment/content-visibility (98%) versus layout mode (1.4×). Same pipeline stage, two orders of magnitude apart. container-type is contain: size wearing a different name.
  • fe-01 — forced synchronous layout is what makes any of this expensive per frame, and it is worth 98× against this module's 1.4×.
  • fe-11 — aspect-ratio and explicit sizing are the primary CLS tools.
  • fe-26 (i18n) — logical properties, and why gap/grid survive RTL where margin hacks do not.
  • fe-30 — when layout scope genuinely cannot be reduced declaratively.
  • fe-38 — container queries are what make design-system components context-independent.
  • bi-08-layout — the layout algorithms from the Blink side, including how constraint propagation and intrinsic sizing are actually implemented.
  • browser-framework-internals.md §12–13 (Layout Engine) — cross for the algorithms themselves.