Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

CSS  Module 10 – Flexbox

How flex items grow, shrink and size

Predict flex item sizes from flex-grow, flex-shrink and flex-basis, decode flex: 1, auto and none, and fix items that overflow because of their minimum size.

  • Intermediate
  • 30 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 30 minutes

What you will learn

  • Predict item sizes from flex-grow, flex-shrink and flex-basis
  • Explain what flex: 1, flex: auto and flex: none mean
  • Fix overflowing flex items caused by the automatic minimum size

Before you start

A flex item’s size along the main axis comes out of three steps. It starts from its flex-basis, which by default is its width, or the width of its content. If the items’ bases add up to less than the line, the free space is shared out in proportion to their flex-grow values; if they add up to more, the missing space is taken back in proportion to flex-shrink multiplied by each basis. Finally every item is kept between its minimum and maximum size, and a flex item’s minimum is, by default, the width of its longest unbreakable content. That last rule is behind many overflowing flex layouts. The flex shorthand sets all three numbers at once: flex: 1 is 1 1 0, flex: auto is 1 1 auto and flex: none is 0 0 auto. Each table sets the widths that Chromium 153, Firefox 155 and WebKit 26.6 drew beside the arithmetic that predicts them.

Where each item starts: flex-basis

The flex basis is the size an item asks for before any sharing. Its default, auto, means “use my width” in a row (or my height in a column), and when that is auto too, the size of the content laid out on one line. A length or a percentage replaces the width for this purpose, and a percentage is a share of the container. The limits min-width and max-width still apply at the end:

flex-basis, width and the limits HTML · basis-width.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>flex-basis, width and the limits</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .row { display: flex; inline-size: 600px; margin-block-end: 8px; background: #eee; }
    .row > div { box-sizing: border-box; background: #e6edfc; }
    #basis-wins > div { inline-size: 300px; flex: 0 1 100px; }      /* a basis other than auto beats the width */
    #auto-uses-width > div { inline-size: 300px; flex: 0 1 auto; }  /* auto: the basis is the width */
    #max-clamps > div { flex: 1 1 200px; max-inline-size: 120px; }  /* the limits still apply */
    #min-clamps { inline-size: 200px; }
    #min-clamps > div { flex: 1 1 0; min-inline-size: 150px; }
    #half > div { flex: 0.5 1 100px; }                             /* factors that add up to less than 1 */
  </style>
</head>
<body>
  <div class="row" id="basis-wins"><div>Item</div></div>
  <div class="row" id="auto-uses-width"><div>Item</div></div>
  <div class="row" id="max-clamps"><div>Item</div></div>
  <div class="row" id="min-clamps"><div>One</div><div>Two</div></div>
  <div class="row" id="half"><div>Item</div></div>
  <script>
    for (const row of document.querySelectorAll('.row')) {
      const widths = [...row.children].map((d) => d.getBoundingClientRect().width);
      console.log(`${row.id}: ${widths.join(', ')} in ${row.getBoundingClientRect().width}px (overflow ${row.scrollWidth - row.clientWidth}px)`);
    }
  </script>
</body>
</html>
Item, in a 600 px row Width
width: 300px; flex: 0 1 100px 100 px
width: 300px; flex: 0 1 auto 300 px
flex: 1 1 200px; max-width: 120px 120 px
two items with flex: 1 1 0; min-width: 150px, in a 200 px row 150 px each
flex: 0.5 1 100px 350 px

All three engines agreed on every row. A basis other than auto beat the width, auto took the width, and the maximum still capped the item. The two items with a 150 px minimum kept it and stuck out of their 200 px row by 100 px: when space runs out, the minimum wins. The last row previews the next section: the item asked for 100 px, 500 px were free, and a lone grow factor of 0.5 took just that fraction of them.

Sharing free space: flex-grow

When the bases add up to less than the line, the difference is the free space, and each item gets a share of it in proportion to its flex-grow. An item with flex-grow: 0, the default, gets nothing. Here are three items, A, B and C, in containers of four widths:

Three items at four widths HTML · grow-shrink.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Three items at four widths</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .row { display: flex; margin-block-end: 8px; background: #eee; }
    .row > div { box-sizing: border-box; background: #e6edfc; }
    .a { flex: 1 1 100px; }   /* grow 1, shrink 1, basis 100px */
    .b { flex: 2 1 200px; }   /* grow 2, shrink 1, basis 200px */
    .c { flex: 0 3 150px; }   /* never grows, shrinks three times as readily */
    .icon { display: inline-block; inline-size: 40px; }   /* C can't be narrower than this */
    .w600 { inline-size: 600px; }
    .w480 { inline-size: 480px; }
    .w360 { inline-size: 360px; }
    .w240 { inline-size: 240px; }
  </style>
</head>
<body>
  <div class="row w600"><div class="a">A</div><div class="b">B</div><div class="c"><span class="icon">C</span></div></div>
  <div class="row w480"><div class="a">A</div><div class="b">B</div><div class="c"><span class="icon">C</span></div></div>
  <div class="row w360"><div class="a">A</div><div class="b">B</div><div class="c"><span class="icon">C</span></div></div>
  <div class="row w240"><div class="a">A</div><div class="b">B</div><div class="c"><span class="icon">C</span></div></div>
  <script>
    for (const row of document.querySelectorAll('.row')) {
      const w = [...row.children].map((d) => `${d.textContent} ${+d.getBoundingClientRect().width.toFixed(2)}`);
      console.log(`${row.getBoundingClientRect().width}px: ${w.join(', ')}`);
    }
  </script>
</body>
</html>

A asks for 100 px, B for 200 px and C for 150 px, 450 px in total. Their grow factors are 1, 2 and 0:

Row A B C
600 px 150 px 300 px 150 px
480 px 110 px 220 px 150 px

At 600 px, 150 px were free: A got one share and B two shares, 50 and 100 px. At 480 px, 30 px were free, so A grew by 10 px and B by 20 px. C never grows. The free space is measured between the items’ outer edges, so padding, borders, margins and gaps come off the line before anything is shared.

Taking space back: flex-shrink

When the bases add up to more than the line, the free space is negative, and the items shrink. This time the share each item gives up is in proportion to its flex-shrink multiplied by its basis (without padding and borders), so a wide item gives up more than a narrow one with the same factor. The default, flex-shrink: 1, lets every item shrink; 0 stops it. At 360 px the three bases are 90 px too wide, and the shrink factors are 1, 1 and 3:

Row A B C
360 px 88 px 176 px 96 px

The weights are 100, 200 and 450, which add up to 750. A gives up 90 × 100 ÷ 750 = 12 px, B 24 px and C 54 px. Shrinking by flex-shrink alone, without the basis, would have taken 18, 18 and 54 px, and a small icon would shrink just as fast as a wide column of text; weighting by the basis keeps the shrinking proportional to the size.

At 480px A and B grow by 10 and 20px; at 360px A, B and C shrink by 12, 24 and 54px. Drawn to scale.Sharing out space, drawn to scale480px container: 30px left overflex-basisA 100B 200C 150after flex-grow 1 : 2 : 0A 110 +10B 220 +20C 150 +0360px container: 90px too littleflex-basis (sticks out past the dashed edge)A 100B 200C 150after flex-shrink × basis 100 : 200 : 450A 88B 176C 96A gives up 12px, B 24px and C 54px:C shrinks 3 × as readily from a bigger basis.

How three flex items grow and shrink

Text description of the diagram

The diagram draws the lesson's three flex items to scale. A has flex: 1 1 100px, B flex: 2 1 200px and C flex: 0 3 150px, so their bases add up to 450px.

  1. In a 480px container, 30px are left over. The free space goes by flex-grow, 1 : 2 : 0, so A grows by 10px to 110px, B by 20px to 220px, and C, whose flex-grow is 0, stays at 150px.
  2. In a 360px container the bases stick out by 90px. The missing space is taken in proportion to flex-shrink times flex-basis: 1 × 100, 1 × 200 and 3 × 150, a ratio of 100 : 200 : 450. A gives up 12px and ends at 88px, B gives up 24px and ends at 176px, and C gives up 54px and ends at 96px, because it shrinks three times as readily and from a larger basis.

Minimums and maximums: when an item is frozen

The 240 px container needs 210 px back. The same arithmetic gives A 72 px, B 144 px and C 24 px, but C holds a 40 px icon and cannot be narrower than that. The browser freezes C at its 40 px minimum and shares the shortfall again among the items that are still flexible: 240 − 40 = 200 px for A and B, which asked for 300 px, so they give up 100 px between them in the ratio 100 : 200. All three engines drew the result:

Row A B C
240 px 66.67 px 133.33 px 40 px

This loop is how the specification resolves flexible lengths: share the space, clamp every item to its minimum and maximum, freeze the items that hit a limit, and share again among the rest until nothing more changes. Here is the loop as a program, run on the same three items:

The space-sharing loop, step by step JavaScript · share_space.mjs
// How one flex line shares out space, following "Resolving Flexible Lengths" in CSS Flexible Box Layout Level 1, for
// the three items of grow-shrink.html. Chromium 153, Firefox 155 and WebKit 26.6 drew exactly these widths.
// A and B hold one letter each, far narrower than any size they reach here, so only C's 40px minimum matters.

const items = [
  { name: 'A', grow: 1, shrink: 1, basis: 100, min: 0, max: Infinity },
  { name: 'B', grow: 2, shrink: 1, basis: 200, min: 0, max: Infinity },
  { name: 'C', grow: 0, shrink: 3, basis: 150, min: 40, max: Infinity },
];

const round = (x) => Math.round(x * 100) / 100;
const sum = (list, f) => list.reduce((total, x) => total + f(x), 0);
const names = (list) => list.map((it) => it.name).join(', ').replace(/, ([^,]*)$/, ' and $1');
// The minimum wins when the two limits disagree.
const clamp = (it, size) => Math.max(it.min, Math.min(it.max, size));

function resolve(container) {
  const growing = sum(items, (it) => it.basis) < container;
  const size = new Map();
  // An item whose factor is 0 keeps its basis: it is frozen from the start.
  for (const it of items) if ((growing ? it.grow : it.shrink) === 0) size.set(it, it.basis);
  for (;;) {
    const open = items.filter((it) => !size.has(it));
    const free = container - sum(items, (it) => (size.has(it) ? size.get(it) : it.basis));
    // Growing shares free space by flex-grow; shrinking takes it away by flex-shrink × basis.
    const weight = (it) => (growing ? it.grow : it.shrink * it.basis);
    const total = sum(open, weight);
    const target = new Map(open.map((it) => [it, it.basis + (free * weight(it)) / total]));
    console.log(`  ${free >= 0 ? `${round(free)}px to share` : `${round(-free)}px too little`} between ${names(open)}, ` +
      `in the ratio ${open.map(weight).join(' : ')} → ${open.map((it) => `${it.name} ${round(target.get(it))}`).join(', ')}`);
    // Clamp every target to its limits. If nothing moved, the sizes are final. Otherwise freeze the items that broke a
    // minimum (when clamping added space overall) or a maximum (when it took space away), and share again.
    const violation = sum(open, (it) => clamp(it, target.get(it)) - target.get(it));
    if (violation === 0) {
      for (const it of open) size.set(it, target.get(it));
      return items.map((it) => `${it.name} ${round(size.get(it))}`).join(', ');
    }
    for (const it of open) {
      const limited = clamp(it, target.get(it));
      if (violation > 0 ? limited > target.get(it) : limited < target.get(it)) {
        console.log(`  ${it.name} can't go ${violation > 0 ? `below its ${it.min}px minimum` : `above its ${it.max}px maximum`}: it is frozen at ${limited}px`);
        size.set(it, limited);
      }
    }
  }
}

console.log(`Every item starts from its flex-basis: ${items.map((it) => it.basis).join(' + ')} = ${sum(items, (it) => it.basis)}px`);
for (const container of [600, 480, 360, 240]) {
  console.log(`${container}px:`);
  console.log(`  final widths: ${resolve(container)}`);
}

Output

Every item starts from its flex-basis: 100 + 200 + 150 = 450px
600px:
  150px to share between A and B, in the ratio 1 : 2 → A 150, B 300
  final widths: A 150, B 300, C 150
480px:
  30px to share between A and B, in the ratio 1 : 2 → A 110, B 220
  final widths: A 110, B 220, C 150
360px:
  90px too little between A, B and C, in the ratio 100 : 200 : 450 → A 88, B 176, C 96
  final widths: A 88, B 176, C 96
240px:
  210px too little between A, B and C, in the ratio 100 : 200 : 450 → A 72, B 144, C 24
  C can't go below its 40px minimum: it is frozen at 40px
  100px too little between A and B, in the ratio 100 : 200 → A 66.67, B 133.33
  final widths: A 66.67, B 133.33, C 40

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node share_space.mjs

A max-width works the same way in the other direction: an item that would grow past its maximum is frozen there, and the others take its share.

The flex shorthand: flex: 1, auto, none

The flex shorthand sets grow, shrink and basis in one declaration, and its keywords cover the common cases:

flex: Grow, shrink, basis The item
initial (default) 0 1 auto keeps its size, may shrink
auto 1 1 auto its size plus a share
none 0 0 auto keeps its size
1, 2 1 1 0, 2 1 0 a share by the number
10rem 1 1 10rem starts at 10rem

initial never grows but may shrink when the row is too narrow, auto starts from the item’s own size and then takes a share of the free space, and none neither grows nor shrinks. A plain number starts the item from nothing, so the line is shared in proportion to the numbers, and a length starts it from that length.

The parts you leave out of the shorthand are filled in differently from the properties’ initial values: a missing grow becomes 1 and a missing basis becomes 0. That is why flex: 1 and flex-grow: 1 behave differently: the first sets the basis to 0, the second leaves it at auto. The specification writes the expansion of flex: 1 as 1 1 0, and all three engines read the basis back as 0%.

A navigation bar with three links of different lengths shows the difference: Home, Products and services, and Contact. Each link has 8 px of padding on both sides, in a 600 px row, and the table gives their widths in px:

flex: 1, auto, none and initial HTML · flex-keywords.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>flex: 1, auto, none and initial</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .row { display: flex; inline-size: 600px; margin-block-end: 8px; background: #eee; }
    .row > a { padding-inline: 8px; background: #e6edfc; }
    #one > a { flex: 1; }
    #auto > a { flex: auto; }
    #none > a { flex: none; }
    #initial > a { flex: initial; }
    #two > a:first-child { flex: 2; }
    #two > a:not(:first-child) { flex: 1; }
  </style>
</head>
<body>
  <nav class="row" id="one"><a href="#a">Home</a><a href="#b">Products and services</a><a href="#c">Contact</a></nav>
  <nav class="row" id="auto"><a href="#a">Home</a><a href="#b">Products and services</a><a href="#c">Contact</a></nav>
  <nav class="row" id="none"><a href="#a">Home</a><a href="#b">Products and services</a><a href="#c">Contact</a></nav>
  <nav class="row" id="initial"><a href="#a">Home</a><a href="#b">Products and services</a><a href="#c">Contact</a></nav>
  <nav class="row" id="two"><a href="#a">Home</a><a href="#b">Products and services</a><a href="#c">Contact</a></nav>
  <script>
    for (const row of document.querySelectorAll('.row')) {
      const first = getComputedStyle(row.firstElementChild);
      const widths = [...row.children].map((a) => +a.getBoundingClientRect().width.toFixed(2));
      console.log(`${row.id}: computed ${first.flexGrow} ${first.flexShrink} ${first.flexBasis}; widths ${widths.join(', ')}`);
    }
  </script>
</body>
</html>
flex Home, Products, Contact
1 200, 200, 200
auto 157.61, 272.33, 170.06
none 58.69, 173.41, 71.14
2, 1, 1 292, 154, 154

These are Chromium’s widths; Firefox and WebKit were within 0.03 px, and flex: initial gave the same widths as none. In the last row Home has flex: 2 and the other two flex: 1. With flex: 1 the links ignored their text and came out equal. With flex: auto each link kept the width of its text and added an equal 98.92 px share of the 296.76 px left over, so long labels stayed longer. none and initial both kept the text widths here, because there was room; the difference is that initial may still shrink when the row gets too narrow. The last row is the surprise: flex: 2 did not make Home twice as wide. The 48 px of padding sat outside the shared space, so the shares were 276, 138 and 138 px of content, and the padding brought them to 292, 154 and 154 px.

When wrapped lines fill unevenly, items with a grow factor on a short last line become much wider than the rest. flex-wrap: balance spreads the items over the lines more evenly, but it is part of the Level 2 draft and only Chrome and Edge 150 support it, so for now a grid is the reliable way to keep equal columns. The patterns lesson of this module measures that case.

The automatic minimum size, and why flex items overflow

The default min-width of a flex item is auto, and for a flex item auto means a content-based minimum: the item’s min-content width, the width of its longest piece of content that cannot break, or the item’s own width when that is set and smaller. A scroll container, an item with overflow set to hidden, auto or scroll, has a minimum of 0 instead. This is why a long word, a URL, a code sample or a checksum can push a flex layout off the screen: the item refuses to get narrower than it.

A sidebar layout on a 360 px phone: a 96 px sidebar, a 16 px gap and a flex: 1 main column with 248 px of room, which holds a 48-character checksum. In the table, break-word and anywhere are values of overflow-wrap on the text, hidden and clip values of overflow on the column, “min 0” is min-inline-size: 0 on the column, and the last two rows hold a pre with overflow-x: auto instead of the checksum. Widths are in px:

One long checksum and every fix HTML · min-size.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>The automatic minimum size of flex items</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .layout { display: flex; gap: 16px; margin-block-end: 8px; }
    .sidebar { flex: 0 0 96px; background: #eee; }
    .main { flex: 1; background: #e6edfc; }
    .main p, .main pre { margin: 0; }

    #break-word p { overflow-wrap: break-word; }
    #anywhere p { overflow-wrap: anywhere; }
    #min-zero .main { min-inline-size: 0; }
    #min-zero-break .main { min-inline-size: 0; }
    #min-zero-break p { overflow-wrap: break-word; }
    #hidden .main { overflow: hidden; }
    #clip .main { overflow: clip; }
    #pre-scroll pre, #pre-scroll-min pre { overflow-x: auto; }
    #pre-scroll-min .main { min-inline-size: 0; }
  </style>
</head>
<body>
  <div class="layout" id="no-fix"><nav class="sidebar">Menu</nav><main class="main"><p>8f14e45fceea167a5a36dedd4bea2543c4f8a4b9e1d9b6e4</p></main></div>
  <div class="layout" id="break-word"><nav class="sidebar">Menu</nav><main class="main"><p>8f14e45fceea167a5a36dedd4bea2543c4f8a4b9e1d9b6e4</p></main></div>
  <div class="layout" id="anywhere"><nav class="sidebar">Menu</nav><main class="main"><p>8f14e45fceea167a5a36dedd4bea2543c4f8a4b9e1d9b6e4</p></main></div>
  <div class="layout" id="min-zero"><nav class="sidebar">Menu</nav><main class="main"><p>8f14e45fceea167a5a36dedd4bea2543c4f8a4b9e1d9b6e4</p></main></div>
  <div class="layout" id="min-zero-break"><nav class="sidebar">Menu</nav><main class="main"><p>8f14e45fceea167a5a36dedd4bea2543c4f8a4b9e1d9b6e4</p></main></div>
  <div class="layout" id="hidden"><nav class="sidebar">Menu</nav><main class="main"><p>8f14e45fceea167a5a36dedd4bea2543c4f8a4b9e1d9b6e4</p></main></div>
  <div class="layout" id="clip"><nav class="sidebar">Menu</nav><main class="main"><p>8f14e45fceea167a5a36dedd4bea2543c4f8a4b9e1d9b6e4</p></main></div>
  <div class="layout" id="pre-scroll"><nav class="sidebar">Menu</nav><main class="main"><pre>npm install --save-dev stylelint stylelint-config-standard</pre></main></div>
  <div class="layout" id="pre-scroll-min"><nav class="sidebar">Menu</nav><main class="main"><pre>npm install --save-dev stylelint stylelint-config-standard</pre></main></div>
  <div class="layout" id="url"><nav class="sidebar">Menu</nav><main class="main"><p>https://example.com/downloads/FlexSizingHandbookSecondEdition.pdf</p></main></div>
  <script>
    // How wide main became, how wide its content is, and how far the layout sticks out past the screen's edge.
    for (const layout of document.querySelectorAll('.layout')) {
      const main = layout.querySelector('.main');
      const content = main.firstElementChild;
      console.log(`${layout.id}: main ${+main.getBoundingClientRect().width.toFixed(2)}px, content ${content.scrollWidth}px, sticks out ${layout.scrollWidth - layout.clientWidth}px`);
    }
  </script>
</body>
</html>
Fix Width Sticks out
none 412 164
break-word 412 164
anywhere 248 0
min 0 248 164
min 0, break-word 248 0
hidden 248 0
clip 412 164
pre 557 309
pre, min 0 248 0

The numbers are Chromium’s; Firefox and WebKit agreed, except that their pre was 559 px wide and stuck out by 311 px. With min-inline-size: 0 alone the column fitted, but the checksum spilled out of it. Adding break-word or using anywhere made the checksum wrap, hidden cut off its end, and in the last row the pre scrolled inside the column.

Three results surprise people. overflow-wrap: break-word changed nothing, because it breaks a word only once the box is already sized, and it does not lower the min-content width, so the flex item kept its 412 px minimum. overflow-wrap: anywhere fixed it alone, because its break points do count for the min-content width. And overflow: clip did not help where overflow: hidden did: clip cuts off the overflow without making a scroll container, so the item keeps its content-based minimum. The reliable fix is min-inline-size: 0 on the flex item together with a decision about the content: let it wrap, let it scroll in its own box, or cut it off.

Engines also differ in where text may break. A long URL without hyphens, https://example.com/downloads/ followed by one long file name, made the same column 505 px wide in Chromium and WebKit but only 277 px in Firefox, because Firefox broke the address after a slash and the others did not. A layout that looks fine in one browser can therefore overflow in another; min-inline-size: 0 with overflow-wrap: anywhere makes them all behave alike.

A media object that truncates its title

A media object, a fixed picture beside text, is the classic place where the minimum size bites. Here the title must stay on one line and end with “…” when it is too long:

A media object with a one-line title CSS · media-object.css
/* A media object: a fixed picture beside text that takes the rest of the line. The title stays on one line and
   ends with "…" when it is too long. */
.media {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
}

.media .avatar {
  flex: none;                 /* 0 0 auto: the picture never grows or shrinks */
  inline-size: 3rem;
  block-size: 3rem;
}

.media .body {
  flex: 1;
  min-inline-size: 0;         /* without this, the one-line title keeps the item as wide as the whole title */
}

.media .title {
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
The same card with and without min-inline-size: 0 HTML · media-object.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>A media object that truncates its title</title>
  <link rel="stylesheet" href="media-object.css">
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .media { margin-block-end: 8px; }
    .avatar { background: #e6edfc; }
    #without-min .body { min-inline-size: auto; }   /* the default, for comparison */
  </style>
</head>
<body>
  <article class="media" id="with-min">
    <div class="avatar"></div>
    <div class="body"><p class="title">Order 4471 shipped: two notebooks, a fountain pen and a desk lamp</p><p>Arrives on Thursday</p></div>
  </article>
  <article class="media" id="without-min">
    <div class="avatar"></div>
    <div class="body"><p class="title">Order 4471 shipped: two notebooks, a fountain pen and a desk lamp</p><p>Arrives on Thursday</p></div>
  </article>
  <script>
    for (const media of document.querySelectorAll('.media')) {
      const body = media.querySelector('.body').getBoundingClientRect();
      const title = media.querySelector('.title');
      console.log(`${media.id} at ${innerWidth}px: body ${+body.width.toFixed(2)}px, title needs ${title.scrollWidth}px and shows ${title.clientWidth}px (${title.scrollWidth > title.clientWidth ? 'cut with an ellipsis' : 'fits'}), sticks out ${media.scrollWidth - media.clientWidth}px`);
    }
  </script>
</body>
</html>

On a 360 px phone, with min-inline-size: 0, the text column was 300 px wide and the 486 px title was cut to 300 px with an ellipsis, in all three engines. Without it, the column grew to the title’s full 486 px and the card stuck out of the screen by 186 px, because white-space: nowrap turned the whole title into one unbreakable line, which became the item’s minimum. The overflow: hidden on the title does not help on its own: the title is not the flex item, its parent is. At 768 px both versions fitted, which is why this bug often hides until someone opens the page on a phone.

CSS Flexbox Generator & Playground Set flex-grow, flex-shrink and flex-basis on live items, then read Why this size? for the same arithmetic as this lesson.

Practise: an item’s automatic minimum width

Exercise · Medium · JavaScript

Work out a flex item's automatic minimum width

A flex item in a row never gets narrower than its minimum width. Write two functions in minimum.mjs and export them. All lengths are in px.

The first, autoMinWidth(item), returns that minimum for an item described by an object:

  • minContent: the item's min-content width (always given);
  • minWidth: 'auto' (the default when it is missing) or a number set with min-width or min-inline-size;
  • width: a number when width or inline-size is set to a length, missing when it is auto;
  • maxWidth: a number when max-width is set, missing otherwise;
  • overflow: 'visible' (the default), 'hidden', 'clip', 'auto' or 'scroll'.

Follow the flexbox specification. A minWidth that is a number is the minimum. With 'auto', an item that is a scroll container (hidden, auto or scroll) has a minimum of 0; any other item has a content-based minimum: the smaller of width (when it is given) and minContent, and never more than maxWidth.

The second, itemWidth(available, item), returns the width a flex: 1 item gets when available px are left for it on its line: the available space, but no wider than maxWidth and never narrower than the item's minimum. When the two limits disagree, the minimum wins.

The sample tests import both functions from minimum.mjs and run in your browser. The 412 px item is the lesson's checksum in a 248 px column.

Starter code · minimum.mjs

/** The minimum width of a flex item in a row (min-width: auto resolves to the automatic minimum). */
export function autoMinWidth(item) {
  // Replace this line with your code.
  return 0;
}

/** The width of a flex: 1 item when `available` px are left for it on its line. */
export function itemWidth(available, item) {
  // Replace this line with your code.
  return available;
}
The sample tests · minimum.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { autoMinWidth, itemWidth } from './minimum.mjs';

const checksum = { minContent: 412 };

test('by default the item is as wide as its longest unbreakable content', () => {
  assert.equal(autoMinWidth(checksum), 412);
  assert.equal(itemWidth(248, checksum), 412);
});

test('min-width: 0 lets the item shrink to the space', () => {
  assert.equal(autoMinWidth({ minContent: 412, minWidth: 0 }), 0);
  assert.equal(itemWidth(248, { minContent: 412, minWidth: 0 }), 248);
  assert.equal(itemWidth(248, { minContent: 412, minWidth: 300 }), 300);
});

test('a scroll container has no content-based minimum, but clip is not one', () => {
  assert.equal(autoMinWidth({ minContent: 412, overflow: 'hidden' }), 0);
  assert.equal(autoMinWidth({ minContent: 412, overflow: 'auto' }), 0);
  assert.equal(autoMinWidth({ minContent: 412, overflow: 'scroll' }), 0);
  assert.equal(autoMinWidth({ minContent: 412, overflow: 'clip' }), 412);
  assert.equal(autoMinWidth({ minContent: 412, overflow: 'visible' }), 412);
});

test('a width set as a length can lower the minimum, never raise it', () => {
  assert.equal(autoMinWidth({ minContent: 412, width: 200 }), 200);
  assert.equal(autoMinWidth({ minContent: 120, width: 300 }), 120);
});

test('max-width caps the minimum and the width', () => {
  assert.equal(autoMinWidth({ minContent: 412, maxWidth: 300 }), 300);
  assert.equal(itemWidth(500, { minContent: 100, maxWidth: 300 }), 300);
  assert.equal(itemWidth(500, { minContent: 100 }), 500);
});

test('when min-width and max-width disagree, the minimum wins', () => {
  assert.equal(itemWidth(200, { minContent: 50, minWidth: 350, maxWidth: 300 }), 350);
});
A hint

Check the cases in order: a number in minWidth first, then a scroll container, then the content-based minimum Math.min(width ?? Infinity, minContent), capped by maxWidth ?? Infinity. itemWidth caps available by maxWidth first and only then raises it to the minimum, so the minimum wins a conflict.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 A 700px row holds four items with no padding, borders or gaps: A and B have flex: 1 1 100px, C has flex: 2 1 100px, and D has flex: none; width: 100px. How wide is C?

    Type a number.

    Show the answer to question 1

    Answer: 250 px (anything from 249 to 251 counts)

    The bases add up to 400px, so 300px are free. D has a grow factor of 0, and A, B and C share the 300px in the ratio 1 : 1 : 2, so C gets 150px on top of its 100px basis: 250px. A and B end at 175px each.

  2. Question 2 of 6 The same four items sit in a 300px row, and all their content is narrower than 60px. How wide is A?

    Type a number.

    Show the answer to question 2

    Answer: 66.67 px (anything from 65.67 to 67.67 counts)

    The bases need 400px, 100px too many. D has flex: none, so it keeps its 100px. A, B and C each have a shrink factor of 1 and a 100px basis, so their weights are equal and each gives up a third of 100px: A ends at about 66.67px.

  3. Question 3 of 6 Which declaration makes three cards share a row equally, whatever the length of their content?

    Choose one answer.

    Show the answer to question 3

    Answer: flex: 1

    flex: 1 sets the basis to 0, so every card starts from nothing and gets an equal share of the line (as long as no card's minimum is larger than its share). flex: auto and flex-grow: 1 keep the basis at auto, so each card starts from its content's width and longer content stays wider. flex: none never grows.

  4. Question 4 of 6 In a 330px row, item X has flex: 2 1 0 and item Y flex: 1 1 0. Each has 15px of padding on both sides and box-sizing: content-box. How wide is X, padding included?

    Type a number.

    Show the answer to question 4

    Answer: 210 px (anything from 209 to 211 counts)

    The padding sits outside the shared space: 4 × 15 = 60px come off the line, leaving 270px. X takes two thirds, 180px of content, plus its 30px of padding: 210px. Y gets 90 + 30 = 120px, so X is not twice as wide.

  5. Question 5 of 6 A flex: 1 column holds a long URL and makes the page scroll sideways on phones. Which declaration on the column lets it become narrower than the URL?

    Choose one answer.

    Show the answer to question 5

    Answer: min-inline-size: 0

    The column's automatic minimum is its min-content width, the width of the URL. min-inline-size: 0 replaces that minimum. overflow-wrap: break-word does not lower the min-content width, a bigger flex-shrink cannot shrink an item past its minimum, and overflow: clip does not make a scroll container, so the minimum stays.

  6. Question 6 of 6 What does flex: 1 expand to?

    Choose one answer.

    Show the answer to question 6

    Answer: flex-grow: 1; flex-shrink: 1; flex-basis: 0

    A single number in the flex shorthand is the grow factor, and the parts left out become a shrink factor of 1 and a basis of 0. Browsers read that basis back as 0%. flex: auto is 1 1 auto, and the initial value is 0 1 auto.

Interview questions

Fresher · What do flex: 1, flex: auto and flex: none mean, and when would you use each?

Each sets flex-grow, flex-shrink and flex-basis together. flex: 1 is 1 1 0: the item starts from a basis of 0, so items with flex: 1 share the line equally whatever their content, which suits equal columns or tabs. flex: auto is 1 1 auto: each item starts from its own size and then takes an equal share of what is left, so longer content stays longer, as in a navigation bar where labels differ. flex: none is 0 0 auto: the item keeps its own size and never grows or shrinks, which suits icons, avatars and buttons beside a flexible column. In a 600 px row of three links with 8 px padding, flex: 1 gave 200 px each, while flex: auto gave 158, 272 and 170 px. The default, flex: initial, is 0 1 auto: no growing, but it may shrink.

Key takeaways

  • An item starts from its flex-basis: auto means its width, and when that is auto, its content’s width.
  • Free space is shared by flex-grow; missing space is taken back by flex-shrink multiplied by the basis, so wider items give up more.
  • Items that hit their minimum or maximum are frozen, and the rest share the space again: at 240 px, 66.67, 133.33 and 40 px.
  • flex: 1 is 1 1 0 (equal shares), flex: auto is 1 1 auto (content plus a share) and flex: none is 0 0 auto (fixed).
  • A flex item’s default minimum is its min-content width. Give it min-inline-size: 0 and decide whether its content wraps, scrolls or is cut off; overflow-wrap: break-word alone does not shrink it.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.