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:
- If
flex-basiscarries a length or a percentage, that is the base size. - If
flex-basis: auto, the size declaration on the main axis is looked at. - 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 ratio: and .
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:
At a container of 400, the weights are , , and
; total 680. The negative 72 units split in this ratio:
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-basisis the size before distribution;autofalls back to the size declaration on the main axis, and if that is alsoauto, to the max-content size.- Writing the
flexshorthand with a single number pulls the base size to zero; writingflex-growon its own leaves the base size atauto, 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: autois 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.