---
title: 'Choosing Between Grid and Flexbox'
source: 'https://academia.sh/en/courses/layout-and-responsive-design/grid-and-flexbox-selection'
course: 'Layout Systems and Responsive Design'
language: en
updated: '2026-08-17T18:09:24+00:00'
license: 'CC BY-SA 4.0'
---

# Choosing Between Grid and Flexbox

Which question each of the two layout models answers, the computed difference in last-row behavior, the cost a spread request creates in flexbox, and the nested use of the two models together.

The previous four lessons established the two layout models separately. Flexbox
distributes on a single axis, grid defines lines on two axes. A row of cards can be laid
out with either; a navigation bar can be built with either. The choice is often made
based on whichever model was learned most recently.

This lesson takes the choice out of habit and ties it to a criterion. The criterion is a
single question: **is the layout decision coming from the content, or from you?** The
answer is hidden not in the two models' names, but in where their calculations start.

## Where the Size Comes From

In flexbox, the calculation starts from the **item**. Every item has a base size; these
sizes are added up, the difference from the container is taken, and the remaining space is
shared out according to growth and shrink factors. The container asks what the items want,
then negotiates. This is why flexbox is called a model that works **from content to
layout**.

In grid, the calculation starts from the **container**. The track definition sets the
lines before any item is placed; items sit onto those lines afterward. A track's size can
be derived from content (`auto`, `min-content`, `max-content`), but this is an option, not
the rule itself. Grid works **from layout to content**.

The distinction determines which question each model answers well:

- If the number and size of items are not known ahead of time, and the remaining space
  needs to be shared among them — the question is single-axis, and flexbox solves it with
  fewer declarations.
- If items need to align relative to each other in both row and column, and the space one
  item takes up determines where another sits — the question is two-axis, and grid is
  needed.

The phrase "single-axis" does not mean items stay on a single line. A flex container with
`flex-wrap: wrap` also produces more than one line. The distinction is this: in flexbox,
every line is computed **on its own**, the lines do not see each other. In grid, every row
shares the same set of lines.

## Last-Row Behavior

The most concrete consequence of this distinction shows up when items do not divide
evenly. The following program lays out the same seven cards in the same container with
the two models.

```js
// choice.mjs — placing the same card row with flexbox and grid
const CONTAINER = 980;   // px
const GAP = 16;          // gap
const BASE = 200;        // flex-basis / minmax lower bound
const CARDS = 7;

// in both models, the number of items that fit one row is found by the same rule
const fitting = Math.max(1, Math.floor((CONTAINER + GAP) / (BASE + GAP)));

// grid: repeat(auto-fill, minmax(200px, 1fr)) -> every track the same size
const trackSize = (CONTAINER - GAP * (fitting - 1)) / fitting;

// flexbox: flex: 1 1 200px + flex-wrap: wrap -> every row shares within itself
function flexRows() {
  const rows = [];
  for (let i = 0; i < CARDS; i += fitting) {
    const count = Math.min(fitting, CARDS - i);
    rows.push({ count, size: (CONTAINER - GAP * (count - 1)) / count });
  }
  return rows;
}

const write = (v) => v.toFixed(1).padStart(6);

console.log(`container=${CONTAINER}px  gap=${GAP}px  base=${BASE}px  cards=${CARDS}`);
console.log(`cards that fit one row: ${fitting}\n`);

console.log("--- grid: repeat(auto-fill, minmax(200px, 1fr)) ---");
let no = 1;
for (let s = 0; s < Math.ceil(CARDS / fitting); s++) {
  const count = Math.min(fitting, CARDS - s * fitting);
  let x = 0;
  const places = [];
  for (let i = 0; i < count; i++) {
    places.push(`c${no++} ${write(x)}..${write(x + trackSize)}`);
    x += trackSize + GAP;
  }
  console.log(`row ${s + 1}: ` + places.join("  "));
}

console.log("\n--- flexbox: flex: 1 1 200px; flex-wrap: wrap ---");
no = 1;
flexRows().forEach((row, s) => {
  let x = 0;
  const places = [];
  for (let i = 0; i < row.count; i++) {
    places.push(`c${no++} ${write(x)}..${write(x + row.size)}`);
    x += row.size + GAP;
  }
  console.log(`row ${s + 1}: ` + places.join("  "));
});

console.log("\n--- right edge of the first column, row by row ---");
const gridFirst = trackSize;
flexRows().forEach((row, s) => {
  const diff = row.size - gridFirst;
  console.log(
    `row ${s + 1}: grid ${write(gridFirst)}  flex ${write(row.size)}  diff ${write(diff)}`,
  );
});
```

```
container=980px  gap=16px  base=200px  cards=7
cards that fit one row: 4

--- grid: repeat(auto-fill, minmax(200px, 1fr)) ---
row 1: c1    0.0.. 233.0  c2  249.0.. 482.0  c3  498.0.. 731.0  c4  747.0.. 980.0
row 2: c5    0.0.. 233.0  c6  249.0.. 482.0  c7  498.0.. 731.0

--- flexbox: flex: 1 1 200px; flex-wrap: wrap ---
row 1: c1    0.0.. 233.0  c2  249.0.. 482.0  c3  498.0.. 731.0  c4  747.0.. 980.0
row 2: c5    0.0.. 316.0  c6  332.0.. 648.0  c7  664.0.. 980.0

--- right edge of the first column, row by row ---
row 1: grid  233.0  flex  233.0  diff    0.0
row 2: grid  233.0  flex  316.0  diff   83.0
```

The first rows are identical. Both models find, with the same arithmetic, that four cards
fit the container, and place the four cards in the same spots. The distinction begins in
the second row.

In grid, the second row uses **the same lines**. Three cards are left, but there are four
tracks; the cards sit in the first three tracks, and the fourth stays empty. The cards'
size does not change: 233 units.

In flexbox, the second row runs its own calculation. Three cards share 980 units, and each
grows to 316 units. The last row fills with cards 83 units wider; the first row's card
edges never line up with the second row's at any point.

This is not a defect, it is the consequence of how the two models are defined. Which one
is correct depends on what is wanted. If the cards are the visual counterpart of a table
and column alignment carries meaning for reading, grid is correct. If the cards are
independent, standalone labels and leaving empty space is not wanted, flexbox's behavior
is the wanted one.

A commonly attempted fix is adding "ghost" items to the last row: invisible boxes to fill
the gap that does not divide evenly. This is a sign of making flexbox do grid's job;
changing models is cheaper than adding an item with no meaning to the document.

## The Cost of a Spread Request

The second criterion shows up when one item is wanted to be **wider** than the others.
Grid writes this with a single declaration: `grid-column: span 2`. In flexbox, its
counterpart is a ratio, and that ratio depends on the track count.

```js
// spread.mjs — the cost of a two-track-wide card in the two models
const GAP = 16;
const BASE = 200;

console.log("minmax(200px, 1fr), gap 16px — size of a card spanning two tracks");
console.log("container  tracks  track size  span 2 size  flex-basis for the same result");

for (const W of [640, 780, 980, 1180, 1420]) {
  const tracks = Math.max(1, Math.floor((W + GAP) / (BASE + GAP)));
  const size = (W - GAP * (tracks - 1)) / tracks;
  const span2 = 2 * size + GAP;           // in grid: grid-column: span 2
  const percent = (span2 / W) * 100;       // basis to write by hand in flexbox
  console.log(
    `${String(W).padStart(9)}  ${String(tracks).padStart(6)}  ` +
      `${size.toFixed(1).padStart(10)}  ${span2.toFixed(1).padStart(11)}  ` +
      `${percent.toFixed(2).padStart(29)}%`,
  );
}

console.log("\n--- the same card written with a single flex-basis value ---");
const CHOSEN = 49.18; // value computed for a 980px width
console.log(`flex: 0 0 ${CHOSEN}%  (the correct value for 980px)`);
console.log("container  applied size  correct at that width  deviation");
for (const W of [640, 780, 980, 1180, 1420]) {
  const tracks = Math.max(1, Math.floor((W + GAP) / (BASE + GAP)));
  const size = (W - GAP * (tracks - 1)) / tracks;
  const correct = 2 * size + GAP;
  const applied = (W * CHOSEN) / 100;
  const deviation = Math.round((applied - correct) * 10) / 10 || 0;
  console.log(
    `${String(W).padStart(9)}  ${applied.toFixed(1).padStart(13)}  ` +
      `${correct.toFixed(1).padStart(22)}  ${deviation.toFixed(1).padStart(9)}`,
  );
}
```

```
minmax(200px, 1fr), gap 16px — size of a card spanning two tracks
container  tracks  track size  span 2 size  flex-basis for the same result
      640       3       202.7        421.3                          65.83%
      780       3       249.3        514.7                          65.98%
      980       4       233.0        482.0                          49.18%
     1180       5       223.2        462.4                          39.19%
     1420       6       223.3        462.7                          32.58%

--- the same card written with a single flex-basis value ---
flex: 0 0 49.18%  (the correct value for 980px)
container  applied size  correct at that width  deviation
      640          314.8                   421.3     -106.6
      780          383.6                   514.7     -131.1
      980          482.0                   482.0        0.0
     1180          580.3                   462.4      117.9
     1420          698.4                   462.7      235.7
```

The first table shows that "two tracks wide" is not a fixed ratio. When the track count is
three, the ratio is close to 66 percent; once it rises to six, it drops to 33 percent. The
same visual request means five different numbers depending on container width.

The second table measures the cost. Once a width is picked and the ratio is written for
it, the deviation at that width is zero; at other widths, it ranges between 100 and 235
units. The card is narrower than it should be in a narrow container, wider than it should
be in a wide one. The only way to hit the correct result at every width is to rewrite the
ratio for width ranges — that is, to hand-compute what grid computes, and imitate it as a
layout decision.

In grid, the `span 2` declaration writes none of this arithmetic; whatever the track size
is, it takes up two tracks and the gap between them, no matter what.

## Order, Spread, and Overlap

The third criterion is whether items see each other.

In flexbox, items are a one-dimensional array. Order can be changed with `order`, but one
item cannot sit on top of another, cannot span two rows at once. In grid, items sit into
cells; placing two items in the same cell is valid, and the two overlap. Layering text
over an image can be written with grid alone, with no positioning needed.

Another distinction continues on the accessibility side. As seen in the grid-placement
lesson, separating visual order from document order breaks focus order. Grid produces this
separation with much less effort: placement by line number ignores document order entirely. In
flexbox, only `order` and reversed directions create the same risk. The model's power does
just as much damage when used incorrectly.

## Nesting the Two Models

Though the criteria look like a comparison, the choice on most pages is not "one or the
other." Large sections of a page are two-axis; the rows of buttons and labels inside a
section are single-axis. Nested use is the consequence of the rule.

```css
/* layout.css — step 5: the two models' division of labor */
.station-layout {           /* two-axis: sections align relative to each other */
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  grid-template-areas:
    "profile      profile"
    "measurements sidebar"
    "location     sidebar";
  gap: var(--spacing-1);
}

.profile             { grid-area: profile; }
.measurement-section { grid-area: measurements; }
.location-section    { grid-area: location; }
.sidebar              { grid-area: sidebar; }

.measurement-cards {        /* two-axis: card edges align column by column */
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--spacing-0);
}

.measurement-cards .summary { grid-column: 1 / -1; }

.card-heading {              /* single-axis: heading and badge on the same line */
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}

.card-heading .unit { margin-inline-start: auto; }
```

Three containers, three separate questions. The page layout aligns sections in both row
and column: grid. The card row wants card edges to line up across rows: grid. The two
items inside a card heading are laid out in a single direction, and the gap between them
grows from content: flexbox.

A boundary appears here. The `.unit` item inside a card heading does not align with the
`.unit` item in a neighboring card; every card runs its own flex calculation. Establishing
that alignment requires the inner container to see the outer grid's lines. The `subgrid`
value lets a nested grid inherit its container's lines instead of defining its own tracks,
and this solves the problem. Whether the value is recognized is tested with a feature
query; query notation is covered in the next topic.

## Order of Decision

The choice can be closed out with three questions, and their order matters.

1. **Will items align relative to each other on two axes?** If the answer is yes, grid.
   This question is decisive enough to make the other two unnecessary.
2. **Is content determining the size?** If the item count is unknown, sizes come from text
   length, and the gap between them should be shared from growing space, flexbox asks for
   fewer declarations.
3. **Will one item span a different area than the others?** If there is a request for
   spreading, overlap, or placement into a specific cell, grid.

None of the criteria carries an ordering along the lines of "grid is new, flexbox is old."
The two models are two separate tools of the same layout layer, and neither replaces the
other.

## Summary

- Flexbox starts its calculation from the item and solves each row on its own; grid
  starts its calculation from the container and applies the same set of lines to every
  row.
- The two models diverge when items do not divide evenly: in grid, the last row's items
  keep the track size; in flexbox, the remaining items grow to share the row.
- One item spanning two tracks is a single declaration in grid; its counterpart in
  flexbox is a ratio that changes with track count and is not correct at every width with
  a single value.
- Overlap, placement into a specific cell, and two-axis alignment are only defined in
  grid; the risk of breaking order grows just as much.
- Grid at the page level, flexbox inside a component is a common division of labor; the
  reason is not habit, it is that the two levels decide on a different number of axes.

## Next Step

Up to this point, the unit of layout has always been a box: a card, a section, a button.
Boxes were placed into lines or distributed along an axis. But the text itself on a page is
also a flow that can be laid out; a long description paragraph stretched into a single line
on a wide screen becomes hard to read. The next lesson covers the model that divides text
flow into columns without splitting it into boxes: how is the relationship between column
count and column width established, and by what rule does content divide between columns?
