---
title: 'Flexing Properties'
source: 'https://academia.sh/en/courses/layout-and-responsive-design/flex-properties'
course: 'Layout Systems and Responsive Design'
language: en
updated: '2026-08-17T18:09:23+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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:

```js
// 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.

```js
// 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 : 1$ ratio: $248 \times 2/4 = 124$ and $248 \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**:

$$
\text{weight}_i = \text{shrink}_i \times \text{basis}_i
$$

At a container of 400, the weights are $1 \times 180 = 180$, $1 \times 140 = 140$, and
$3 \times 120 = 360$; total 680. The negative 72 units split in this ratio:
$-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:

```js
// 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.

```css
/* 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.
