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.
What is flexbox actually computing when I write flex: 1, and why does one long item push everything else out of the way?
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.
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.
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).
- The title with a long string does not truncate — it pushes the buttons out of the container and off screen, and
overflow: hiddenon the title does nothing until you also setmin-width: 0(Intrinsic Sizing and the Automatic Minimum). justify-contentstops doing anything the moment an item hasflex-grow, because there is no free space left to distribute.- Switching
flex-directionfromrowtocolumnmakesjustify-contentandalign-itemsswap meanings, so every alignment rule in the component is now describing the other axis. flex: 1andflex: 1 1 autobehave 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: centeron a column of text makes each child shrink to its content instead of filling the width, because the defaultstretchwas the thing making them full-width.
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 makesrowandcolumna 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 fromwidth/height, or from its content whenauto). That is clamped bymin-*/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 toflex-shrinkweighted by base size — a bigger item gives up more, which is why shrinking looks proportional and growing looks equal. flex: 1is shorthand forflex: 1 1 0%— base size zero, so all the space is free space and items end up equal regardless of content.flex: autoisflex: 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: autoon 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-contentdistributes leftover free space along the main axis.align-itemsandalign-selfalign within the cross-axis line.align-contentdistributes space between lines, and therefore does nothing on a single-line (non-wrapping) container.orderandrow-reversechange 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-basisso 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 shrinksThe 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).
.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 */.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.
1<div class="toolbar">2 <h2 class="title" title="Q3 revenue reconciliation — EMEA, draft 4">3 Q3 revenue reconciliation — EMEA, draft 44 </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.
semantics None of order, row-reverse or column-reverse touches the DOM or the accessibility tree — they change paint order only.
| Tab | Moves in DOM order, which is now different from the order the user can see |
| Shift+Tab | Moves backwards in DOM order — visually forwards, if the row is reversed |
| Screen-reader browse keys | Read in accessibility-tree order, derived from the DOM, not from the layout |
- — 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.
- — 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.
The item is in a row with siblings. What determines its width?
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.
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.
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.
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(ormin-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
gapover 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 0when items should end up equal andflex: 1 1 autowhen 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
orderor*-reverseto 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-reverseandcolumn-reversechange 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: wrapplusmin-width: 0is usually the whole fix. - Vertically centring a control with
align-items: centermust 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
- 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 ismin-width: 0on 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-basisagainst an indefinite container main size, which resolves tocontentand produces layout that depends on measurement rather than on the number you wrote. align-items: centerused to centre a single child, which also removes the defaultstretchand 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).
- Items whose content arrives asynchronously change their base size when it lands, so free space is redistributed and every sibling moves. Reserve a
flex-basisif 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.
- 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: ellipsishides 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
ordercan 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.
- "
flex: 1makes items equal width." It makes them equal only because itsflex-basisis0.flex: 1 1 autoon the same items produces different widths, and both are "flex: 1" in casual speech. - "
flex-shrinkis the mirror offlex-grow." Shrinking is weighted by base size and growing is not, so identical factors give different-looking results in the two directions. - "
align-contentcentres my items." Not on a single-line container. With no wrapping there is only one line, andalign-contenthas 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.
- "
orderreorders 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
- The Elements panel's
flexbadge 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-widthon an item that will not shrink.autothere 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).
- 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: wrapcosts nothing until it is needed. - In a right-to-left context,
rowreverses on its own because it follows writing direction. That is correct behaviour, and it meansrow-reversein 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).
flex: 1 1 0gives 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: 0makes 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 atitle, 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-basisagainst 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.
- — Software Design —
flex: 1 1 0versus1 1 autois 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.