Lesson 04 / 23
Grid Placement
Explicit placement of items by line number, the cursor rule of the auto-placement algorithm, the difference between sparse and dense modes, the formation of implicit tracks, and the separation of visual order from document order.
Contents
The previous lesson established the grid and named its areas. Named-area notation makes a layout readable, but it does not suit every layout: measurement cards whose count is not known ahead of time cannot be named one by one.
This lesson answers two questions. How is an item placed explicitly by line, and by what rule are items with no placement declaration distributed?
Resolving to Line Numbers
An item’s placement is given on two axes with four lines: grid-column declares the
column lines, grid-row the row lines. Each takes a start and end separated by a slash.
// line.mjs — resolving grid-column notation to line numbers const COLUMNS = 4; // 4 tracks -> 5 lines const LAST_LINE = COLUMNS + 1; // the line referred to as -1 // a negative line number counts from the end: -1 -> LAST_LINE, -2 -> LAST_LINE-1 const line = (n) => (n > 0 ? n : LAST_LINE + 1 + n); function resolve(notation) { const [start, end] = notation.split("/").map((p) => p.trim()); let startLine = line(Number(start)); let endLine; if (end === undefined) endLine = startLine + 1; // single value: one track else if (end.startsWith("span")) endLine = startLine + Number(end.split(" ")[1]); else endLine = line(Number(end)); if (endLine < startLine) [startLine, endLine] = [endLine, startLine]; // reversed notation is fixed return { startLine, endLine, tracks: endLine - startLine }; } console.log(`${COLUMNS} column tracks -> lines 1..${LAST_LINE} (same lines -${LAST_LINE}..-1)`); for (const y of ["1 / 3", "2 / -1", "1 / span 2", "-3 / -1", "3", "4 / 2"]) { const { startLine, endLine, tracks } = resolve(y); console.log(`grid-column: ${y.padEnd(12)} -> line ${startLine} to line ${endLine}, ${tracks} track${tracks === 1 ? "" : "s"}`); } console.log("\n--- equivalence of negative and positive numbers ---"); for (let n = 1; n <= LAST_LINE; n++) { console.log(`line ${n} = line ${n - LAST_LINE - 1}`); }
4 column tracks -> lines 1..5 (same lines -5..-1) grid-column: 1 / 3 -> line 1 to line 3, 2 tracks grid-column: 2 / -1 -> line 2 to line 5, 3 tracks grid-column: 1 / span 2 -> line 1 to line 3, 2 tracks grid-column: -3 / -1 -> line 3 to line 5, 2 tracks grid-column: 3 -> line 3 to line 4, 1 track grid-column: 4 / 2 -> line 2 to line 4, 2 tracks --- equivalence of negative and positive numbers --- line 1 = line -5 line 2 = line -4 line 3 = line -3 line 4 = line -2 line 5 = line -1
Five forms of notation, one rule: every declaration resolves to two line numbers, and the track count between them is the difference.
The notation 2 / -1 runs from the second line to the last line. The track count is not
written in the declaration; if the grid grows to six columns, the same declaration spans
five tracks. Counting from the end lets the layout be written independently of track
count.
The span keyword declares a length instead of a line: 1 / span 2 goes two tracks
from the start. Writing only span 2 on an item whose start line is unknown is also
valid; the item is then auto-placed but occupies two tracks.
The last line shows a detail: if the end line is smaller than the start, the two swap. The
notation 4 / 2 is not an error, it resolves between the second and fourth lines.
The shorthand for all four declarations is the grid-area property, and its order is
unconventional: row start, column start, row end, column end. The previous lesson gave an
area a name with the notation grid-area: profile; the same property also takes four
lines instead of a name.
Automatic Placement
Items with no placement declaration are distributed by the auto-placement algorithm. The algorithm holds a cursor, walks items in document order, and places each item into the first gap it fits in, starting from the cursor.
The grid-auto-flow declaration says which axis to advance on: the value row fills a
row and drops to the next one, column fills a column and moves to the one beside it. A
third value, dense, allows the cursor to move backward.
// placement.mjs — sparse and dense modes of the auto-placement algorithm const COLUMNS = 3; // span = how many column tracks it will occupy const items = [ { name: "temperature", span: 1 }, { name: "humidity", span: 2 }, { name: "wind", span: 2 }, { name: "pressure", span: 1 }, { name: "precipitation", span: 1 }, { name: "snow", span: 3 }, { name: "sunshine", span: 1 }, { name: "soil", span: 2 }, ]; function place(dense) { const filled = []; // filled[row][column] = item name const addRow = () => { filled.push(new Array(COLUMNS).fill(null)); }; const fits = (r, c, span) => { if (c + span > COLUMNS) return false; while (filled.length <= r) addRow(); for (let i = 0; i < span; i++) if (filled[r][c + i]) return false; return true; }; const result = []; let cursorRow = 0, cursorColumn = 0; for (const o of items) { let r = dense ? 0 : cursorRow; let c = dense ? 0 : cursorColumn; while (!fits(r, c, o.span)) { c += 1; if (c + o.span > COLUMNS) { c = 0; r += 1; } } while (filled.length <= r) addRow(); for (let i = 0; i < o.span; i++) filled[r][c + i] = o.name; result.push({ name: o.name, row: r + 1, column: c + 1, span: o.span }); if (!dense) { // in sparse mode the cursor does not go back cursorRow = r; cursorColumn = c + o.span; if (cursorColumn >= COLUMNS) { cursorRow = r + 1; cursorColumn = 0; } } } return { filled, result }; } function print(title, { filled, result }) { console.log(`\n=== ${title} ===`); for (const y of result) { console.log(` ${y.name.padEnd(13)} row ${y.row}, column ${y.column}..${y.column + y.span - 1} (grid-column: ${y.column} / span ${y.span})`); } console.log(` row count: ${filled.length}`); const emptyCells = filled.flat().filter((h) => h === null).length; console.log(` empty cells: ${emptyCells}`); for (const row of filled) { console.log(" | " + row.map((h) => (h ?? "-").padEnd(13)).join(" | ") + " |"); } } console.log(`${COLUMNS}-column grid, ${items.length} items, spans: ${items.map((o) => o.span).join(" ")}`); print("grid-auto-flow: row (sparse, default)", place(false)); print("grid-auto-flow: row dense", place(true));
3-column grid, 8 items, spans: 1 2 2 1 1 3 1 2 === grid-auto-flow: row (sparse, default) === temperature row 1, column 1..1 (grid-column: 1 / span 1) humidity row 1, column 2..3 (grid-column: 2 / span 2) wind row 2, column 1..2 (grid-column: 1 / span 2) pressure row 2, column 3..3 (grid-column: 3 / span 1) precipitation row 3, column 1..1 (grid-column: 1 / span 1) snow row 4, column 1..3 (grid-column: 1 / span 3) sunshine row 5, column 1..1 (grid-column: 1 / span 1) soil row 5, column 2..3 (grid-column: 2 / span 2) row count: 5 empty cells: 2 | temperature | humidity | humidity | | wind | wind | pressure | | precipitation | - | - | | snow | snow | snow | | sunshine | soil | soil | === grid-auto-flow: row dense === temperature row 1, column 1..1 (grid-column: 1 / span 1) humidity row 1, column 2..3 (grid-column: 2 / span 2) wind row 2, column 1..2 (grid-column: 1 / span 2) pressure row 2, column 3..3 (grid-column: 3 / span 1) precipitation row 3, column 1..1 (grid-column: 1 / span 1) snow row 4, column 1..3 (grid-column: 1 / span 3) sunshine row 3, column 2..2 (grid-column: 2 / span 1) soil row 5, column 1..2 (grid-column: 1 / span 2) row count: 5 empty cells: 2 | temperature | humidity | humidity | | wind | wind | pressure | | precipitation | sunshine | - | | snow | snow | snow | | soil | soil | - |
Both maps place the same eight items into the same three columns, and the result is identical for the first six items. The distinction starts at the seventh item.
In sparse mode, once the snow item has filled the fourth row, the cursor has moved to
the fifth row and does not go back. Even though the third row has two empty cells, the
sunshine item drops to the fifth row. The gap remaining is not a defect, it is the
consequence of the rule: it preserves the link between document order and visual order.
In dense mode, the search for every item starts from the beginning of the grid.
sunshine fills the gap in the third row. The soil item wants two tracks and does not
fit there, so it drops to the fifth row — dense mode fills gaps, but it does not force an
item that does not fit.
In the end, both modes leave two cells empty; what changes is which item is where. Dense mode does not always leave less gap, it only fills earlier.
The Cost of Dense Mode
In dense mode, the sunshine item appears above the snow item, which comes after it
in the document. Visual order and document order have diverged.
This has concrete consequences. When a user navigating by keyboard presses tab, focus follows document order and jumps backward on screen. A screen reader reads document order and does not match the visual layout. Two users perceive the same page in two different orders.
The same warning applies to every reordering produced by the order declaration,
flex-direction: row-reverse, and explicit line placement. The rule is: if visual order
and document order diverge, it should be made certain that the order in the document is
correct as the reading order; visual layout is built on top of that order, it does not
replace it.
For measurement cards, the decision is easy: if the cards’ order does not carry the importance of the measurement type, dense mode can be used. If order carries meaning — for instance, if measurements are in chronological order — sparse mode is kept.
Implicit Tracks
A track definition bounds the grid but does not bound the items. Placing eight items required five rows, yet the track definition did not write a row count. When there are more items than defined, the grid grows on its own, and the tracks that form this way are called implicit tracks.
The ones defined are called the explicit grid, the ones that form on their own the
implicit grid. The distinction has two consequences. First, the size of implicit
tracks comes not from the grid-template-* declaration but from grid-auto-rows and
grid-auto-columns; if not written, the size is content. Second, counting from the end
() is only defined for the explicit grid; implicit tracks are not part of that
numbering.
/* layout.css — step 4: measurement cards */ .measurement-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-auto-rows: minmax(8rem, auto); gap: var(--spacing-0); } .measurement-cards .wide { grid-column: span 2; } .measurement-cards .summary { grid-column: 1 / -1; grid-row: 1; }
The grid-auto-rows: minmax(8rem, auto) declaration gives every implicit row a lower
bound: cards with short content still come out at least eight base units tall, and cards
with long content can grow. The notation 1 / -1 spans the summary card across the entire
row and stays correct even as the track count changes with screen width.
One warning: a card carrying span 2 will ask for two tracks even when auto-fill
produces only one. In this case, the item does not overflow the explicit grid; the grid
adds an implicit column to the right and the layout breaks. If cards are going to be given
a span, the widths at which the track count is at least two need to be constrained
separately — the tool for this conditional notation is the queries covered in the next
topic.
Alignment Works at Two Levels
In grid, alignment answers two separate questions. First: where does an item sit within
its own area? justify-self declares this on the inline axis, align-self on the block
axis; justify-items and align-items written on the container give the default for all
items. Since the default value is stretch, an item fills its area.
Second: if the tracks’ total is smaller than the container, where does the grid itself
sit? justify-content and align-content declare this, and the set of values is the same
as flexbox’s. If the tracks are defined with fr, no free space remains, so these
declarations have no visible effect; on fixed-size tracks, they are visible.
In flexbox, justify-content distributed items; in grid, it distributes tracks. The same
name applies to a different object in the two models, and when this distinction is
confused, the declaration appears to have no effect.
Summary
- Every placement declaration resolves to two line numbers;
spandeclares a length, negative numbers count from the end, and reversed lines swap places. - Auto-placement puts items into the first gap they fit in, starting from the cursor, in document order; in sparse mode the cursor does not go back and gaps remain.
- Dense mode fills gaps but separates visual order from document order; focus order and screen readers keep following document order.
- Items beyond what is defined produce implicit tracks, whose size comes from
grid-auto-rowsandgrid-auto-columns; counting from the end does not cover implicit tracks. - In grid,
justify-itemsandalign-itemsplace an item inside its area,justify-contentandalign-contentplace the tracks inside the container.
Next Step
Both layout models are now established: one distributes on a single axis, the other defines lines on two axes. There are situations the same layout can be built with either, and the choice is often made out of habit. The next lesson ties this choice to a criterion: which question is answered with fewer declarations and less fragility by which model?
To keep your progress and take notes, Log in
My notes
Log in to take notes.