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

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

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.

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

```js
// 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$) is only defined for the explicit grid; implicit tracks are not part of that
numbering.

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