LayoutGENERALENGINE-SPECIFIC

Flexbox: One Axis at a Time

A distribution algorithm, not a property list: base sizes, free space, grow and shrink along the main axis, alignment along the cross axis.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

What is flexbox actually computing when I write flex: 1, and why does one long item push everything else out of the way?

The user intent

Someone wants a toolbar: a title on the left, a couple of actions on the right, everything vertically centred, and the title truncating rather than shoving the buttons off the edge.

The obvious build

Flexbox is the layout mode where things go in a row. Set display: flex, add justify-content and align-items until it looks right, and put flex: 1 on whatever should be big.

Why it breaks

The title with a long string does not truncate — it pushes the buttons out of the container and off screen, and overflow: hidden on the title does nothing until you also set min-width: 0 (Intrinsic Sizing and the Automatic Minimum).

How it breaks in a real browser
  • The title with a long string does not truncate — it pushes the buttons out of the container and off screen, and overflow: hidden on the title does nothing until you also set min-width: 0 (Intrinsic Sizing and the Automatic Minimum).
  • justify-content stops doing anything the moment an item has flex-grow, because there is no free space left to distribute.
  • Switching flex-direction from row to column makes justify-content and align-items swap meanings, so every alignment rule in the component is now describing the other axis.
  • flex: 1 and flex: 1 1 auto behave differently for items with different content — one makes items equal, the other makes their *growth* equal — and the difference only shows up once real content arrives.
  • align-items: center on a column of text makes each child shrink to its content instead of filling the width, because the default stretch was the thing making them full-width.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • A flex container lays out its children along a main axis, set by flex-direction. The perpendicular axis is the cross axis. Every flex property names an axis, not a screen direction — which is what makes row and column a single model rather than two.
  • Sizing along the main axis is a distribution algorithm. First each item gets a flex base size from flex-basis (or from width/height, or from its content when auto). That is clamped by min-*/max-* to give a hypothetical main size.
  • The container sums the hypothetical main sizes. The difference from the container's main size is free space. If positive, it is handed out in proportion to flex-grow. If negative, it is taken back in proportion to flex-shrink weighted by base size — a bigger item gives up more, which is why shrinking looks proportional and growing looks equal.
  • flex: 1 is shorthand for flex: 1 1 0% — base size zero, so all the space is free space and items end up equal regardless of content. flex: auto is flex: 1 1 auto — content is measured first and only the *surplus* is shared, so items stay proportional to their content.
  • Every flex item has an automatic minimum size. min-width: auto on a row item resolves to its min-content size, so an item never shrinks below its longest unbreakable word. This is the single most common flexbox surprise and it is deliberate: it stops content vanishing (Intrinsic Sizing and the Automatic Minimum).
  • justify-content distributes leftover free space along the main axis. align-items and align-self align within the cross-axis line. align-content distributes space between lines, and therefore does nothing on a single-line (non-wrapping) container.
  • order and row-reverse change visual position only. The DOM order, the tab order, and the order a screen reader reads are untouched, and the divergence is the accessibility hazard of this layout mode.

What this makes the browser do

And which of it is avoidable.

  • Flex layout needs at least two passes over its items: measure intrinsic contributions, then resolve flexible lengths. A deeply nested flex tree multiplies that — an item that is itself a flex container is measured, sized, and measured again.
  • Intrinsic measurement means laying out the content at min-content and max-content widths, which for a text item means line breaking twice before the real line breaking (Intrinsic Sizing and the Automatic Minimum).
  • Wrapping (flex-wrap: wrap) adds line-building on top: items are assigned to lines, each line is resolved independently, then lines are aligned. Cost grows with item count, not with container size.
  • Most of this is avoidable by not nesting flex containers where a single grid would do, and by giving items an explicit flex-basis so their intrinsic size never has to be measured.

Main axis, cross axis — and nothing about left or right

The reframe that makes flexbox stop being a list of properties: there is no "horizontal" or "vertical" in this model. There is a main axis, chosen by flex-direction, and a cross axis perpendicular to it. Every property is defined against one of those two, so row and column are the same layout with the axes swapped.

This is also why the same CSS behaves correctly in a right-to-left locale without a single override: the main axis of a row container follows the writing direction, so "start" means the side text starts on, not the left.

flex-direction: row                          flex-direction: column
(main = inline axis)                        (main = block axis)

  main start ------------------> main end     +----------+  <- main start
  +--------+--------+--------------+  ^       |   item   |      |
  | item A | item B |   item C     |  |       +----------+      |
  +--------+--------+--------------+  | cross |   item   |      | main axis
       <-- gap             free space |       +----------+      |
                                      v       |   item   |      v
  ^                                           +----------+  <- main end
  |                                           <---------->
  justify-content: along the MAIN axis         cross axis: align-items

  the algorithm, once:

    1. flex-basis (or width, or content)   ->  flex base size, per item
    2. clamp by min-* / max-*              ->  hypothetical main size
       note: min-width defaults to AUTO on a flex item = min-content
    3. sum them, compare to container      ->  free space (+ or -)
    4. free space > 0  ->  share by flex-grow          (unweighted)
       free space < 0  ->  take back by flex-shrink    (weighted by base size)
    5. leftover free space                 ->  justify-content
    6. cross axis                          ->  align-items / align-self

  flex: 1        =  1 1 0%     basis 0   -> everything is free space -> EQUAL items
  flex: auto     =  1 1 auto   basis content -> share the SURPLUS -> proportional
  flex: none     =  0 0 auto   rigid: neither grows nor shrinks

The toolbar that will not truncate

This is the canonical flexbox bug, and it is worth writing out because the fix looks arbitrary until you know the rule. A title that should shrink and truncate instead pushes the buttons out of the container. Nothing in the CSS says "do not shrink"; the automatic minimum size does.

Every flex item has min-width: auto in a row, and auto resolves to the item's min-content size — the width of its longest unbreakable run. The item will shrink to that and stop. overflow: hidden and text-overflow: ellipsis never get a chance to apply, because the box is never smaller than its content (Intrinsic Sizing and the Automatic Minimum).

Truncating title, fixed actions
Pushes the buttons off screen
.toolbar { display: flex; align-items: center; gap: 1rem; }
.title  { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.actions { flex: none; }

/* the title still refuses to go below its longest word */
Shrinks, then truncates
.toolbar { display: flex; align-items: center; gap: 1rem; }
.title {
  flex: 1 1 0;
  min-inline-size: 0;      /* opt out of the automatic minimum size */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.actions { flex: none; }   /* 0 0 auto: never grows, never shrinks */

min-width: auto on a flex item resolves to min-content, so the box never gets small enough for overflow to clip anything. min-inline-size: 0 opts out of that floor; flex: none is what actually protects the buttons, because without it they are shrinkable too.

And the truncated value still has to be reachable
1<div class="toolbar">
2 <h2 class="title" title="Q3 revenue reconciliation — EMEA, draft 4">
3 Q3 revenue reconciliationEMEA, draft 4
4 </h2>
5 <div class="actions">
6 <button type="button">Share</button>
7 <button type="button">Save</button>
8 </div>
9</div>

The ellipsis is paint, not content: the full string is still in the DOM and still announced in full. A sighted mouse user is the only one who loses information, which is why the title (or a tooltip component) is part of the pattern rather than a nicety.

Sizing an item is one decision, written three ways

Almost every flexbox review comment is really about this choice. The shorthand hides it, so writing the longhand — or at least knowing which shorthand you picked — is the difference between a row that behaves as content changes and one that only works with the mock data.

accessibility specA flex row whose visual order differs from its DOM orderThe reordering hazard

semantics None of order, row-reverse or column-reverse touches the DOM or the accessibility tree — they change paint order only.

TabMoves in DOM order, which is now different from the order the user can see
Shift+TabMoves backwards in DOM order — visually forwards, if the row is reversed
Screen-reader browse keysRead in accessibility-tree order, derived from the DOM, not from the layout
Focus
  • Focus jumps to a visually distant control with no cue that it will, which is disorienting with a magnifier and impossible to predict with a switch device.
  • If the row scrolls horizontally, focusing a visually-first item scrolls the container back to the start mid-sequence.
Announces
  • Position information ("3 of 5") comes from the DOM, so an assistive-technology user is told a position that contradicts the screen.

usually broken by Using order or a *-reverse direction to fix a visual mistake that is really a markup-order mistake. Reorder the markup: it is the only change that moves the visual order, the tab order and the reading order together.

How should this flex item be sized along the main axis?

The item is in a row with siblings. What determines its width?

`flex: none` (`0 0 auto`)

when The item must keep its natural size: an icon button, a fixed-width action group, a badge

cost It cannot shrink, so it is a hard floor on the container's minimum width and a candidate cause of horizontal scrolling at 400% zoom.

`flex: 1 1 0` (with `min-width: 0`)

when Siblings should end up the same size regardless of content — equal columns, a segmented control

cost Content is ignored: a column with far more content gets the same width and must truncate or scroll.

`flex: 1 1 auto`

when Items should stay proportional to their content and share only the surplus — tags, chips, breadcrumb segments

cost Sizes now depend on data, so a single long value changes the whole row and layout differs per user.

`flex: 0 1 <basis>`

when You want a specific size that may shrink under pressure — a sidebar that gives ground before the content does

cost The basis is a magic number that has to be maintained alongside whatever it was derived from.

How to build it

Most important first.

  • Choose flex when the content should decide the sizes along one axis — a toolbar, a row of chips, a form row. Choose grid when the *layout* should decide sizes in two axes (Grid: Two Dimensions at Once).
  • Set min-width: 0 (or min-inline-size: 0) on any flex item that must be allowed to shrink or truncate. Treat it as part of the truncation pattern, not as a workaround.
  • Prefer gap over margins between items. It does not collapse, it does not add an edge at the ends, and it is the same property in grid (Normal Flow, Overflow and Margin Collapsing).
  • Use flex: 1 1 0 when items should end up equal and flex: 1 1 auto when they should stay proportional to their content. Writing the three values out is worth the extra characters — the shorthand hides the decision.
  • Do not use order or *-reverse to fix DOM order. If the reading order is wrong, the markup is wrong; reordering visually leaves keyboard users navigating an order they cannot see (Keyboard Operability).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • order, row-reverse and column-reverse change paint order without changing DOM order. A keyboard user then tabs from the visually-last control to the visually-first, and there is no visual cue that this will happen. Reorder the markup instead.
  • The same divergence affects screen readers, which follow the accessibility tree — derived from the DOM, not from the visual arrangement. Two users are then working from two different orders of the same interface (The Accessibility Tree).
  • Flex containers make raw text an anonymous flex item, which is fine visually and removes it from the inline formatting context. Any styling that depended on it being an inline run — such as an inline link wrapping mid-sentence — stops applying.
  • At 400% zoom a non-wrapping row is the classic reflow failure: the row cannot get narrower than the sum of its min-content sizes, so the page scrolls horizontally. flex-wrap: wrap plus min-width: 0 is usually the whole fix.
  • Vertically centring a control with align-items: center must not shrink its hit target. Centre the box, keep the padding — the target size is the padding box, not the text (The Box Model).

What can go wrong

Failure modes
  • The unshrinkable item: a long word, a <pre>, a table or a nested scroll container refuses to go below its min-content size and blows the row out. The fix is min-width: 0 on the item, and it must be applied at every level of nesting.
  • The disappearing justify-content: it silently stops applying once anything grows, so a spacing rule appears to be ignored rather than overridden.
  • Percentage flex-basis against an indefinite container main size, which resolves to content and produces layout that depends on measurement rather than on the number you wrote.
  • align-items: center used to centre a single child, which also removes the default stretch and collapses that child to its content size — usually noticed as "my divider disappeared".
  • Deep flex nesting used as a general layout language: correct, and quietly expensive, because each level re-measures its children (Layout Thrashing).
What can arrive out of order
  • Items whose content arrives asynchronously change their base size when it lands, so free space is redistributed and every sibling moves. Reserve a flex-basis if the row must not shift (Visual Stability).
  • A web font arriving late changes every text item's intrinsic contribution, which can flip a row from fitting to wrapping after first paint.
Security
  • Flexbox distributes space based on content sizes, so untrusted content controls layout. A single long token can push controls off screen, which is a UI-redress vector rather than a cosmetic issue (Clickjacking and Framing).
  • Truncation is visual only. text-overflow: ellipsis hides characters on screen and leaves them fully present in the DOM, in the accessibility tree and in the clipboard — never use it to withhold anything.
  • Reordering with order can put a destructive action visually where a safe one used to be while leaving the DOM order intact, which makes both keyboard activation and automated testing disagree with what the user sees.
Misreads
  • "flex: 1 makes items equal width." It makes them equal only because its flex-basis is 0. flex: 1 1 auto on the same items produces different widths, and both are "flex: 1" in casual speech.
  • "flex-shrink is the mirror of flex-grow." Shrinking is weighted by base size and growing is not, so identical factors give different-looking results in the two directions.
  • "align-content centres my items." Not on a single-line container. With no wrapping there is only one line, and align-content has nothing to distribute.
  • "Flexbox is one-dimensional so it is less capable." It is one-dimensional so it can let *content* determine sizes along that axis, which is the thing grid deliberately does not do.
  • "order reorders the content." It reorders the pixels. Everything derived from the DOM — tab order, screen-reader order, copy and paste — keeps the original order.

Measuring it, and what changes in the field

How you would see this
  • The Elements panel's flex badge opens the flexbox overlay, which draws the container, the items, the free space and the gaps. Seeing where the free space actually is answers most "why is it not centred" questions immediately.
  • In the Computed tab, look at the resolved min-width on an item that will not shrink. auto there is the automatic minimum size doing exactly what it is specified to do.
  • The Performance panel attributes layout time to the container. Repeated layout on a flex subtree during interaction usually means something is writing a size in a loop (Layout Thrashing).
Slow device, slow network, large data, old tab
  • With long content — a user-supplied name, a URL, a translated string — the automatic minimum size becomes the dominant force in the row. Design the row for the longest realistic string, not the design mock's.
  • On a narrow viewport, a non-wrapping row is a horizontal scroll waiting to happen. flex-wrap: wrap costs nothing until it is needed.
  • In a right-to-left context, row reverses on its own because it follows writing direction. That is correct behaviour, and it means row-reverse in an RTL locale means the opposite of what its author usually intended (Internationalization).
  • With hundreds of items, flex wrapping cost grows linearly and the intrinsic measurement passes dominate. That is the point at which virtualization is the answer rather than a different layout mode (List Virtualization).
What this costs
  • flex: 1 1 0 gives visually even columns and ignores content, so a column with much more content gets the same width and scrolls or truncates. Even is not always right.
  • min-width: 0 makes items shrinkable and therefore makes it possible for content to disappear behind an ellipsis. You are trading a broken layout for hidden information, and the hidden information needs a title, a tooltip, or a way to see the whole value.
  • Flexbox for whole-page layout works and costs you two-dimensional alignment: rows cannot align to each other's columns, which is precisely the thing grid does (Grid: Two Dimensions at Once).

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe flex layout algorithm — base size, hypothetical main size, free space, grow and shrink, cross-axis alignment — is specified in CSS Flexible Box Layout Level 1 and is interoperable across Blink, Gecko and WebKit today.
  • ENGINE-SPECIFICHistorical differences persist in old-content territory: percentage flex-basis against an indefinite container, and flex items whose children are tables or replaced elements, were resolved differently by older Safari and older Blink. Modern engines agree, but a bug report from an old iOS WebView is often one of these rather than a mistake in your CSS.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Software Design — flex: 1 1 0 versus 1 1 auto is a policy about who owns a size, the container or the content, and it is the same shape of decision as any other ownership question.