Lesson 05 / 23
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.
Contents
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.
// 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.
// 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.
/* 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.
- 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.
- 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.
- 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?
To keep your progress and take notes, Log in
My notes
Log in to take notes.