Skip to content
academia.sh

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 (1-1) 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; span declares 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-rows and grid-auto-columns; counting from the end does not cover implicit tracks.
  • In grid, justify-items and align-items place an item inside its area, justify-content and align-content place 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.

Start typing to search.

↑↓ Esc navigate · open · close