Skip to content
academia.sh

Lesson 02 / 23

Flexing Properties

Where an item's base size comes from, how positive free space is divided by growth factors, how negative free space is shared with scaled shrink factors, and the freezing of items that hit a minimum size.

Contents

The previous lesson took the items’ main-axis sizes as fixed and always found the free space positive. In a real filter bar, sizes come from content; once the container narrows, the sum of the three fields exceeds the container and the free space drops below zero.

This lesson covers the three declarations that govern that sharing: flex basis, flex grow factor, and flex shrink factor. Together, the three answer a single question: what fraction of the difference between the container and the items’ total is written to which item?

Three Declarations and Their Shorthand

flex-basis is an item’s main-axis size before distribution. The calculation starts from this value; growth and shrinkage are added to or subtracted from it.

flex-grow is the factor for the share taken from positive free space. Its default is zero: if an item is not given a factor, it stays at its base size.

flex-shrink is the factor for the share taken on from negative free space. Its default is one: if an item is not given a factor, it still shrinks. The two defaults being asymmetric is deliberate — overflow is more harmful than growing on its own.

The three are written in a single declaration, and the shorthand’s expansion differs from values written on their own:

// shorthand.mjs — the flex shorthand's expansion into three declarations
const shorthands = [
  ["flex: 1", "1 1 0%"],
  ["flex: 2", "2 1 0%"],
  ["flex: auto", "1 1 auto"],
  ["flex: initial", "0 1 auto"],
  ["flex: none", "0 0 auto"],
  ["flex: 0 0 12rem", "0 0 12rem"],
];
for (const [notation, expansion] of shorthands) {
  console.log(`${notation.padEnd(16)} -> flex-grow flex-shrink flex-basis = ${expansion}`);
}
flex: 1          -> flex-grow flex-shrink flex-basis = 1 1 0%
flex: 2          -> flex-grow flex-shrink flex-basis = 2 1 0%
flex: auto       -> flex-grow flex-shrink flex-basis = 1 1 auto
flex: initial    -> flex-grow flex-shrink flex-basis = 0 1 auto
flex: none       -> flex-grow flex-shrink flex-basis = 0 0 auto
flex: 0 0 12rem  -> flex-grow flex-shrink flex-basis = 0 0 12rem

The first line deserves attention: the notation flex: 1 pulls the base size to zero. Writing flex-grow: 1 on its own leaves the base size at auto. The two notations look the same but give a different result; the numeric counterpart of the difference is computed at the end of the lesson.

Where the Base Size Comes From

flex-basis: auto is a redirection, not a size: the item looks at its size declaration on the main axis (width in the row direction, height in the column direction). If that is also auto, the size comes from content.

The order is:

  1. If flex-basis carries a length or a percentage, that is the base size.
  2. If flex-basis: auto, the size declaration on the main axis is looked at.
  3. If that is also auto, the item’s max-content size is used: the width it would take up if the text were written on a single line with no wrapping at all.

The base size is subject to the box-sizing value. If the border-box declaration established in the Visual Presentation with CSS course is in effect, padding and border are inside the base size; otherwise they are added on top and the total overflows.

The Arithmetic of Distribution

The following program solves the previous lesson’s filter bar at five different container widths. The items’ base sizes, growth and shrink factors, and minimum sizes are fixed; the only thing that changes is the container.

// flexing.mjs — computes the grow and shrink shares of flex items
const GAP = 16;

// each item's: basis, grow factor, shrink factor, minimum size
const items = [
  { name: "search", basis: 180, grow: 2, shrink: 1, min: 120 },
  { name: "date", basis: 140, grow: 1, shrink: 1, min: 100 },
  { name: "status", basis: 120, grow: 1, shrink: 3, min: 64 },
];

// CSS's "resolving flexible lengths" step: iterative distribution with frozen items
function resolve(container) {
  const n = items.length;
  const rawFreeSpace = container - items.reduce((t, o) => t + o.basis, 0) - GAP * (n - 1);
  const growing = rawFreeSpace > 0;

  const state = items.map((o) => ({ ...o, size: o.basis, frozen: false }));
  let round = 0;

  while (true) {
    round += 1;
    const flexible = state.filter((d) => !d.frozen);
    if (flexible.length === 0) break;

    const used = state.reduce((t, d) => t + (d.frozen ? d.size : d.basis), 0);
    const remaining = container - used - GAP * (n - 1);

    if (growing) {
      const totalFactor = flexible.reduce((t, d) => t + d.grow, 0);
      if (totalFactor === 0) break;
      for (const d of flexible) d.size = d.basis + remaining * (d.grow / totalFactor);
    } else {
      // shrinking: the factor is scaled by the basis: a small box shrinks less
      const totalWeight = flexible.reduce((t, d) => t + d.shrink * d.basis, 0);
      if (totalWeight === 0) break;
      for (const d of flexible) d.size = d.basis + remaining * ((d.shrink * d.basis) / totalWeight);
    }

    // an item that drops below its minimum is frozen and distribution restarts
    const violated = flexible.filter((d) => d.size < d.min);
    if (violated.length === 0) break;
    for (const d of violated) { d.size = d.min; d.frozen = true; }
    if (round > 10) break;
  }

  return { rawFreeSpace, round, state };
}

const format = (x) => x.toFixed(1).padStart(6);

console.log("basis sizes:", items.map((o) => `${o.name}=${o.basis}`).join("  "), ` gap=${GAP}x2`);
console.log("factors:     ", items.map((o) => `${o.name} grow=${o.grow} shrink=${o.shrink} min=${o.min}`).join("  |  "));

for (const container of [720, 560, 480, 400, 320]) {
  const { rawFreeSpace, round, state } = resolve(container);
  const total = state.reduce((t, d) => t + d.size, 0) + GAP * (items.length - 1);
  console.log(`\ncontainer=${container}  raw free space=${rawFreeSpace}  round=${round}`);
  for (const d of state) {
    const share = d.size - d.basis;
    const sign = share >= 0 ? "+" : "-";
    console.log(`  ${d.name.padEnd(6)} size=${format(d.size)}  share=${sign}${Math.abs(share).toFixed(1).padStart(5)}  ${d.frozen ? "frozen (minimum size)" : ""}`);
  }
  console.log(`  total (gap included) = ${total.toFixed(1)}`);
}
basis sizes: search=180  date=140  status=120  gap=16x2
factors:      search grow=2 shrink=1 min=120  |  date grow=1 shrink=1 min=100  |  status grow=1 shrink=3 min=64

container=720  raw free space=248  round=1
  search size= 304.0  share=+124.0  
  date   size= 202.0  share=+ 62.0  
  status size= 182.0  share=+ 62.0  
  total (gap included) = 720.0

container=560  raw free space=88  round=1
  search size= 224.0  share=+ 44.0  
  date   size= 162.0  share=+ 22.0  
  status size= 142.0  share=+ 22.0  
  total (gap included) = 560.0

container=480  raw free space=8  round=1
  search size= 184.0  share=+  4.0  
  date   size= 142.0  share=+  2.0  
  status size= 122.0  share=+  2.0  
  total (gap included) = 480.0

container=400  raw free space=-72  round=1
  search size= 160.9  share=- 19.1  
  date   size= 125.2  share=- 14.8  
  status size=  81.9  share=- 38.1  
  total (gap included) = 400.0

container=320  raw free space=-152  round=3
  search size= 124.0  share=- 56.0  
  date   size= 100.0  share=- 40.0  frozen (minimum size)
  status size=  64.0  share=- 56.0  frozen (minimum size)
  total (gap included) = 320.0

Every one of the five blocks ends its last line with the same check: the sizes’ total, gaps included, equals the container. That is flexbox’s promise; how it keeps it changes.

Positive Free Space Divides by Factors

In the first three blocks, the free space is positive, and the share looks only at the ratio of the growth factors. At a container of 720, the 248-unit space splits in a 2:1:12 : 1 : 1 ratio: 248×2/4=124248 \times 2/4 = 124 and 248×1/4=62248 \times 1/4 = 62.

A common mistake here is assuming the share is proportional to the item’s own size. It is not. date and status have different base sizes (140 and 120), but since their factors are equal, both get 62 units; the 20-unit difference between them is preserved after distribution too.

The third block shows a boundary case: at a container of 480, the free space is only 8 units and the shares drop to 4 and 2. The factor ratio stays the same, what shrinks is the dividend.

If the factor total is less than one, not all of the free space is distributed. If every one of the three items is given flex-grow: 0.2, the total is 0.6, and only sixty percent of the free space is shared; the rest sits as a gap at the end of the container.

Negative Free Space Divides by Scaled Factors

In the fourth block, the free space is negative and the calculation changes. The shrink share is determined by the factor’s product with the base size:

weighti=shrinki×basisi\text{weight}_i = \text{shrink}_i \times \text{basis}_i

At a container of 400, the weights are 1×180=1801 \times 180 = 180, 1×140=1401 \times 140 = 140, and 3×120=3603 \times 120 = 360; total 680. The negative 72 units split in this ratio: 72×360/68038.1-72 \times 360/680 \approx -38.1 is written to the status item’s share.

The scaling is not arbitrary. If factors were used independent of scale, the smaller of two items with equal factors would shrink by the same absolute amount as the larger one and hit zero first. A scaled factor balances shrinkage as a percentage: every item carries a load proportional to its own size, and the factor makes that load heavier or lighter.

That the status item’s shrink factor is three is a design decision: the status badge is the area that can shrink the most without losing its text. The opposite holds for the search field; if flex-shrink: 0 were written on it, it would never shrink and the entire load would fall on the other two.

The gap does not shrink. In the calculation, 32 units are fixed on every line; the gap value does not enter the flexing distribution, it is subtracted from the container up front.

The Minimum Size Restarts the Distribution

The last block is solved in three rounds, and the reason is written in the output’s last column.

In the first round, proportional distribution brings the status item down to 39.5 units; this is below the item’s minimum size of 64. The item is pinned to that value and frozen: it no longer takes part in the distribution, and its size is subtracted from the container. The second round repeats for the remaining two items, and this time the date item drops to 98, violating its own lower bound; it freezes too. In the third round, a single flexible item remains, and the entire remaining shortfall is written to it: search stops at 124 units.

This iteration is flexbox’s defined algorithm. It has two consequences. First, one item hitting its lower bound increases the others’ share; the layout’s shrinking behavior is not linear. Second, if every item hits its lower bound, the total overflows the container and the item overflows — flexbox cannot keep its promise.

Where the lower bound comes from is a separate matter. If a flex item’s min-width is auto, the bound is set to the item’s min-content size: the narrowest form the text can be broken into, that is, the width of its longest word. This is why an item carrying a long word ends up wider than expected, and the fix is removing the constraint by writing min-width: 0.

Zero Basis versus Content Basis

The difference left open at the start of the lesson can be computed twice in the same container:

// basis.mjs — two different basis values with the same factors
const CONTAINER = 600, GAP = 16;
const names = ["search", "date", "status"];
const content = [180, 140, 120];
const factor = [2, 1, 1];

for (const mode of ["0%", "auto"]) {
  const bases = mode === "0%" ? [0, 0, 0] : content;
  const free = CONTAINER - bases.reduce((a, b) => a + b, 0) - GAP * 2;
  const total = factor.reduce((a, b) => a + b, 0);
  const sizes = bases.map((b, i) => b + free * (factor[i] / total));
  console.log(`flex-basis: ${mode.padEnd(5)} -> ${sizes.map((s, i) => `${names[i]}=${s.toFixed(1)}`).join("  ")}`);
}
flex-basis: 0%    -> search=284.0  date=142.0  status=142.0
flex-basis: auto  -> search=244.0  date=172.0  status=152.0

When the base size is zero, the container’s entire width is distributed, and the final sizes reflect only the factor ratio: two items with equal factors are equal in width. When the base size is auto, content sizes are set aside first and the remainder is distributed; content differences carry through to the result.

The selection rule is: if a content-independent ratio between items is wanted, the base size is zero; if free space should be shared while content differences are preserved, auto is written. In the filter bar, the second is correct: the fields’ natural widths carry meaning.

/* layout.css — step 2: measurement filter */
.measurement-filter {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  align-items: end;
}

.measurement-filter .search  { flex: 2 1 180px; min-inline-size: 120px; }
.measurement-filter .date    { flex: 1 1 140px; }
.measurement-filter .status  { flex: 1 3 120px; }

Summary

  • flex-basis is the size before distribution; auto falls back to the size declaration on the main axis, and if that is also auto, to the max-content size.
  • Writing the flex shorthand with a single number pulls the base size to zero; writing flex-grow on its own leaves the base size at auto, and the two notations give different results.
  • Positive free space divides only by the ratio of the growth factors; the items’ base sizes do not enter the share, and differences between them are preserved after distribution.
  • Negative free space divides by the factor’s product with the base size; scaling keeps small items from running out before large ones.
  • An item that drops below its minimum size is frozen at that value, and distribution runs again with the remaining items; on a flex item, min-width: auto is a bound equal to the min-content size.

Next Step

Flexbox works on a single axis: it distributes in one direction, and only aligns in the other. That is enough for a filter bar, but the measurement page as a whole needs regions aligned in two axes — the profile block, the table, and the sidebar need to sit relative to each other in both row and column. The next lesson establishes the second layout model, which defines rows and columns ahead of time.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close