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

# The Grid Model

Establishing a grid formatting context, the distinction between track and line, the fractional unit dividing free space, the hidden lower bound the min-content size places on a share, and naming areas.

Flexbox distributes on a single axis: it shares out in one direction and only aligns in
the other. That is enough for a filter bar. The measurement page as a whole needs regions
aligned on two axes: the profile block, the measurement table, and the sidebar need to sit
relative to each other in both row and column.

This lesson establishes the layout model that defines rows and columns **ahead of time**.
In flexbox, placement arises from the items' sizes; in grid, an empty line layout is
defined first and items are placed into that layout.

## The Grid Formatting Context

When `display: grid` is declared on a box, the box becomes a **grid container**; its
direct children are placed as **grid items**. As with flexbox, the container itself stays
a block box in flow, the children's `display` gets blockified, and their margins do not
collapse.

The distinction is in what is declared on the container. In flexbox, a single direction is
declared; in grid, the **track definition** for both axes is written:

```css
.station-layout {
  display: grid;
  grid-template-columns: 240px 2fr 1fr;
  grid-template-rows: auto 1fr;
  gap: 24px;
}
```

## Track, Line, and Cell

Four concepts are kept separate from each other.

A **grid track** is the band between two adjacent lines: a column or a row. The
declaration above defines three column tracks and two row tracks.

A **grid line** is the line separating tracks. Three column tracks produce **four** lines;
lines are numbered from 1 starting at the beginning. The same lines are also referred to
with negative numbers counting from the end: the last line is $-1$, the one before it
$-2$. Extending an item to the end of the grid does not require knowing the track count;
$-1$ is enough.

A **grid cell** is the intersection of a row track and a column track; the smallest unit
in a grid.

A **grid area** is a rectangular region bounded by four lines; it can span one or more
cells. An item's placement is always an area.

Lines can be named, and the name is written inside square brackets, between the entries
of the track definition:

```css
.station-layout {
  display: grid;
  grid-template-columns: [edge-start] 240px [content-start] 2fr 1fr [edge-end];
}
```

A named line can be used instead of a number, and it makes rewriting placement
declarations unnecessary when the track count changes.

## The Fractional Unit Divides Free Space

`fr` is a unit specific to grid: the **fractional unit**. It is not a length; it says how
many shares the **remaining space** divides into once fixed tracks and gaps are subtracted.

```js
// track.mjs — how the fr unit divides free space, and its hidden lower bound
const GAP = 24;

// track definition: fixed lengths are set aside first, the rest divides into fr shares
const tracks = [
  { notation: "240px", fixed: 240, fr: 0 },
  { notation: "2fr", fixed: 0, fr: 2 },
  { notation: "1fr", fixed: 0, fr: 1 },
];

const gapTotal = GAP * (tracks.length - 1);
const fixedTotal = tracks.reduce((t, s) => t + s.fixed, 0);
const frTotal = tracks.reduce((t, s) => t + s.fr, 0);

function shares(container) {
  const free = container - fixedTotal - gapTotal;
  return tracks.map((s) => s.fixed + (s.fr / frTotal) * free);
}

const CONTAINER = 960;
console.log(`grid-template-columns: ${tracks.map((s) => s.notation).join(" ")}   gap: ${GAP}px   container: ${CONTAINER}px`);
console.log(`fixed=${fixedTotal}  gap=${gapTotal}  free space to divide=${CONTAINER - fixedTotal - gapTotal}  fr total=${frTotal}`);
shares(CONTAINER).forEach((o, i) => console.log(`  ${tracks[i].notation.padEnd(6)} -> ${o.toFixed(1)}px`));

console.log("\n--- fr is a share of the free space, not of the container ---");
for (const extraFixed of [0, 240, 480]) {
  const one = (CONTAINER - extraFixed - gapTotal) / 3;
  console.log(`fixed track ${String(extraFixed).padStart(3)}px -> 1fr = ${one.toFixed(1)}px  (2fr = ${(2 * one).toFixed(1)}px)`);
}

// in a narrow container, the fr share drops below the track's min-content size
const NARROW = 600;
const minContent = [90, 180, 160];   // each track's longest unbreakable content
console.log(`\n--- narrow container (${NARROW}px): 1fr = minmax(auto, 1fr) ---`);
let total = 0;
shares(NARROW).forEach((share, i) => {
  const min = tracks[i].fr > 0 ? minContent[i] : 0;
  const applied = Math.max(share, min);
  total += applied;
  console.log(`  ${tracks[i].notation.padEnd(6)} share=${share.toFixed(1).padStart(6)}  min content=${String(min).padStart(3)}  applied=${applied.toFixed(1).padStart(6)}`);
});
console.log(`  tracks+gap = ${(total + gapTotal).toFixed(1)}px, container = ${NARROW}px, overflow = ${(total + gapTotal - NARROW).toFixed(1)}px`);

console.log(`\n--- same container with minmax(0, 1fr) ---`);
let total2 = 0;
shares(NARROW).forEach((share, i) => {
  total2 += share;
  const name = tracks[i].fr > 0 ? `minmax(0, ${tracks[i].fr}fr)` : tracks[i].notation;
  console.log(`  ${name.padEnd(16)} applied=${share.toFixed(1).padStart(6)}`);
});
console.log(`  tracks+gap = ${(total2 + gapTotal).toFixed(1)}px, overflow = ${(total2 + gapTotal - NARROW).toFixed(1)}px`);
```

```
grid-template-columns: 240px 2fr 1fr   gap: 24px   container: 960px
fixed=240  gap=48  free space to divide=672  fr total=3
  240px  -> 240.0px
  2fr    -> 448.0px
  1fr    -> 224.0px

--- fr is a share of the free space, not of the container ---
fixed track   0px -> 1fr = 304.0px  (2fr = 608.0px)
fixed track 240px -> 1fr = 224.0px  (2fr = 448.0px)
fixed track 480px -> 1fr = 144.0px  (2fr = 288.0px)

--- narrow container (600px): 1fr = minmax(auto, 1fr) ---
  240px  share= 240.0  min content=  0  applied= 240.0
  2fr    share= 208.0  min content=180  applied= 208.0
  1fr    share= 104.0  min content=160  applied= 160.0
  tracks+gap = 656.0px, container = 600px, overflow = 56.0px

--- same container with minmax(0, 1fr) ---
  240px            applied= 240.0
  minmax(0, 2fr)   applied= 208.0
  minmax(0, 1fr)   applied= 104.0
  tracks+gap = 600.0px, overflow = 0.0px
```

The first block shows the calculation's order: from a 960-unit container, the fixed track
(240) and two gaps (48) are subtracted first; the remaining 672 units divide into three
shares. `2fr` takes two shares (448), `1fr` one share (224).

The second block corrects a commonly made reading mistake. `1fr` is not one-third of the
container; it is one-third of the **free space**. As the fixed track grows, the same
`1fr` declaration's value drops from 304 to 144. The track definition itself has not
changed; what changed is the dividend.

If the share total is less than one, not all of the free space is distributed:
`0.5fr 0.5fr` uses half the free space, the rest sits at the end of the grid. This behavior
is consistent with the same declaration's counterpart in flexbox.

## The Share's Hidden Lower Bound

The third block shows the most common cause of overflow in grid definitions. When the
container drops to 600 units, the last track's share computes to 104 units, but there is
160 units' worth of unbreakable content inside the track — a long measurement code, or a
table. The track stops at 160, and the total exceeds the container by 56 units.

The reason is this: `1fr`, in its full expansion, means `minmax(auto, 1fr)`. When the lower
bound is `auto`, the track cannot drop below its content's **min-content size**. If the
share falls below this bound, the bound wins.

The fourth block gives the fix: if the lower bound is explicitly pulled to zero, the track
follows the share and the overflow disappears. The cost is that content can now overflow
inside the track; overflow management is handled separately in that track, with the
declarations established in the Visual Presentation with CSS course.

This is grid's counterpart to the `min-width: auto` constraint in flexbox. In both models,
the source of the problem is the same: when the content size is larger than the computed
share, the constraint wins.

## Repetition and Gap

Instead of repeating the same track definition by hand, `repeat()` is written. What is
interesting is that the repeat count can be given not as a number but as one of two
keywords: `auto-fill` and `auto-fit`. Both produce the most tracks that fit the container;
the distinction is in what happens when the produced tracks are **empty**.

```js
// repetition.mjs — track count produced by auto-fill and auto-fit
const MIN = 220;   // minmax(220px, 1fr)
const GAP = 24;
const ITEM_COUNT = 5;

// most tracks that fit the container: n*min + (n-1)*gap <= width
function trackCount(width) {
  const n = Math.floor((width + GAP) / (MIN + GAP));
  return Math.max(1, n);
}

const size = (width, n) => (width - GAP * (n - 1)) / n;

console.log("repeat(auto-fill | auto-fit, minmax(220px, 1fr))   gap: 24px   item count: 5");
console.log("width     tracks  auto-fill size   filled tracks  auto-fit size");
for (const w of [320, 500, 760, 1000, 1240, 1600]) {
  const n = trackCount(w);
  const filled = Math.min(n, ITEM_COUNT);
  const fill = size(w, n);
  const fit = size(w, filled);
  console.log(
    String(w).padStart(8) +
    String(n).padStart(7) +
    (fill.toFixed(1) + "px").padStart(16) +
    String(filled).padStart(12) +
    (fit.toFixed(1) + "px").padStart(15)
  );
}

console.log("\n--- boundary: exact widths where the track count increases ---");
for (let n = 1; n <= 6; n++) {
  const threshold = n * MIN + (n - 1) * GAP;
  console.log(`minimum width for ${n} track${n === 1 ? "" : "s"}: ${threshold}px`);
}

console.log("\n--- same calculation with a fixed track count: repeat(4, minmax(220px, 1fr)) ---");
for (const w of [500, 760, 1000]) {
  const n = 4;
  const share = size(w, n);
  const applied = Math.max(share, MIN);
  const overflow = applied * n + GAP * (n - 1) - w;
  console.log(`width ${String(w).padStart(4)} -> share ${share.toFixed(1)}px, applied ${applied.toFixed(1)}px, overflow ${overflow.toFixed(1)}px`);
}
```

```
repeat(auto-fill | auto-fit, minmax(220px, 1fr))   gap: 24px   item count: 5
width     tracks  auto-fill size   filled tracks  auto-fit size
     320      1         320.0px           1        320.0px
     500      2         238.0px           2        238.0px
     760      3         237.3px           3        237.3px
    1000      4         232.0px           4        232.0px
    1240      5         228.8px           5        228.8px
    1600      6         246.7px           5        300.8px

--- boundary: exact widths where the track count increases ---
minimum width for 1 track: 220px
minimum width for 2 tracks: 464px
minimum width for 3 tracks: 708px
minimum width for 4 tracks: 952px
minimum width for 5 tracks: 1196px
minimum width for 6 tracks: 1440px

--- same calculation with a fixed track count: repeat(4, minmax(220px, 1fr)) ---
width  500 -> share 107.0px, applied 220.0px, overflow 452.0px
width  760 -> share 172.0px, applied 220.0px, overflow 192.0px
width 1000 -> share 232.0px, applied 232.0px, overflow 0.0px
```

`minmax(a, b)` gives a track's lower and upper bound: the track does not drop below `a`,
does not rise above `b`. `minmax(220px, 1fr)` means "at least 220 units, take the rest as
a share," and the repeat count is derived from this lower bound.

The track count follows directly: the minimum width needed for $n$ tracks is
$n \times 220 + (n-1) \times 24$ units, and the count grows as the container exceeds this
value. The second block gives these thresholds; the numbers in the first block line up
exactly with them — a 1000-unit container gets four tracks, because the fourth threshold
is 952 and the fifth is 1196.

The distinction appears in the last line. A 1600-unit container is wide enough for six
tracks, but there are only five items to place. `auto-fill` **keeps the sixth track
empty**; items stay at 246.7 units and an empty column remains on the right. `auto-fit`
**collapses** the empty track; the track and its gap disappear, the five items share the
remaining space and rise to 300.8 units.

The selection criterion is content-related. If card width should stay consistent and items
change across different pages, `auto-fill` is correct: the same card is the same width on
every page. If items should fill the container in every case, `auto-fit` is correct.

The third block shows why a fixed repeat count overflows on narrow screens:
`repeat(4, …)` unconditionally produces four tracks, holds the lower bound at 220 units, and
gives 452 units of overflow in a 500-unit container. The repeat keywords read this
condition from the container; they adapt the track count without needing the media queries
covered in the next topic.

## Naming Areas

The track definition establishes the grid; where items fall is a separate declaration. The
most readable way is to name areas and draw the placement as text in the container:

```css
/* layout.css — step 3: page grid */
.station-layout {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  grid-template-rows: auto auto 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; }
```

Each row of the string corresponds to a row track, each column to a column track. The same
name appearing in two cells says the area spans those cells; `sidebar` extends across two
rows. Writing a dot instead of a name leaves the cell empty.

This notation's value is that the layout becomes **visible** in the style file: the
alignment of the lines draws the grid itself. An item's order in the document and its
position in the grid are independent of each other — this distinction's accessibility
consequences are covered in the next lesson.

## Summary

- `display: grid` takes a track definition on two axes; a track is the band between two
  lines, a cell is the intersection of two tracks, and an area is the rectangular region
  bounded by four lines.
- Lines are numbered from 1 at the start and $-1$ from the end; they can be tied to a
  name, and a name keeps placement declarations intact when the track count changes.
- `fr` is not a length but a fractional unit: it divides the space remaining after fixed
  tracks and gaps are subtracted, so it gives a ratio of the free space, not the
  container.
- Because `1fr` expands to `minmax(auto, 1fr)`, a track does not drop below its
  min-content size; to prevent overflow, the lower bound is explicitly zeroed with
  `minmax(0, 1fr)`.
- `repeat(auto-fill, …)` produces every track that fits and keeps empty ones, `auto-fit`
  collapses empty tracks and distributes the space to the remaining items.

## Next Step

The track definition established the grid, and named areas drew the placement. But where
do items with no area declaration fall? What happens if the container has more items than
are defined? The next lesson covers the declarations that place items explicitly by line
and the automatic algorithm that places the leftover ones.
