Ch. 5 · HTML & CSS

CSS Grid Tracks and Minmax Sizing

How fr shares free space, what minmax() and auto mean for a track, and the math behind repeat(auto-fill, minmax(220px, 1fr)) at real widths.

~5 min readintermediateupdated Oct 7, 2026

CSS grid’s sizing functions read like a small language: 200px 1fr 2fr, minmax(0, 1fr), repeat(auto-fill, minmax(220px, 1fr)). Each piece is simple, but people often memorise the last one as an incantation for “responsive cards” without being able to say what width each card will be. Interviewers like to ask exactly that, so this note works through the arithmetic with real numbers.

How fr shares space

The fr unit is a share of the free space: whatever is left in the grid container after fixed-size tracks and gaps are taken out.

.layout {
  display: grid;
  grid-template-columns: 200px 1fr 2fr;
  gap: 20px;
  width: 900px;
}
CSS

Step by step:

container width          900
fixed track               200
gaps (2 × 20)              40
free space               900 − 200 − 40 = 660
total fr                    3
1fr                       660 ÷ 3 = 220
columns                  200 | 220 | 440
Text

The second column is 220px and the third is 440px. Widen the container to 1200px and the free space becomes 960px, giving 320px and 640px; the 200px column doesn’t move.

The difference from percentages is that fr is computed after gaps. With grid-template-columns: 25% 75% and a 20px gap, the tracks add up to 100% plus 20px and the grid overflows by 20px. With 1fr 3fr the same proportions fit exactly.

The hidden minimum in 1fr

A bare 1fr track is shorthand for minmax(auto, 1fr). The minimum, auto, resolves to the min-content size of the items in that track: the width of the longest word, an image’s width, a <pre> block’s longest line.

So 1fr 1fr is “two equal columns” only while the content allows it. Put a long unbreakable URL in the first column and that column grows to fit the URL; the second column gets what’s left. If even that doesn’t fit, the grid overflows its container. This is the grid version of the flexbox min-width: auto problem.

The fix is to make the minimum explicit:

.two-up {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
CSS

minmax(0, 1fr) says the track can shrink to zero, so both columns stay exactly equal, and long content overflows inside its own cell, where you can handle it with overflow-wrap: anywhere or overflow-x: auto. Many design systems use minmax(0, 1fr) as their default column definition for this reason.

minmax() and the other sizing keywords

minmax(min, max) gives a track a range: it will be at least min and at most max. The browser first gives every track its minimum, then hands out remaining space up to each maximum, with fr maximums sharing whatever is left over.

The keywords you’ll meet inside it:

  • auto: as a minimum, the min-content size of the items (subject to their min-width). As a maximum, the max-content size, but auto tracks also stretch to fill leftover space when no fr tracks exist.
  • min-content: the narrowest the content can be without overflowing (every soft wrap opportunity taken).
  • max-content: the width the content would take with no wrapping at all.
  • fit-content(300px): grows like max-content but stops at 300px, then wraps. Good for a sidebar sized by its labels, with a cap.

A useful one for a page shell is grid-template-columns: fit-content(280px) minmax(0, 1fr): a sidebar that’s as narrow as its links allow (up to 280px) and a main column that takes the rest without being pushed by wide content.

The responsive card grid, worked through

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 16px;
}
CSS

auto-fill asks the browser to create as many columns as will fit, using each track’s minimum (220px) for the fitting calculation. Then every track gets its 1fr share of what’s left, so they stretch equally to fill the row.

The number of columns n is the largest integer where n × 220 + (n − 1) × 16 doesn’t exceed the container width. Here’s what that gives at three widths:

container   columns   check                         each column
1000px      4         4×220 + 3×16 = 928 ≤ 1000    (1000 − 48) ÷ 4 = 238px
700px       3         3×220 + 2×16 = 692 ≤ 700     (700 − 32) ÷ 3 ≈ 222.67px
400px       1         2×220 + 16 = 456 > 400       400px
Text

No media queries, and every card is at least 220px wide and at most just under 456px (the single-column case). The upper bound matters: at a 450px container you get one column 450px wide, because two 220px columns plus the gap need 456px. That’s where the layout looks least balanced, and it’s a good number to mention when asked about the pattern’s weaknesses.

There’s one more weakness. On a screen narrower than 220px plus the container’s padding (a 240px-wide smartwatch or a narrow split-screen pane), the single 220px column overflows. The common hardening is minmax(min(220px, 100%), 1fr), which drops the minimum to the container’s full width when the container is smaller than 220px.

auto-fill versus auto-fit

They differ only when there are fewer items than columns. Say the 1000px container holds two cards.

With auto-fill, the browser still creates four 238px tracks. The two cards occupy the first two, and the other two tracks sit empty. The cards are 238px each, and the right half of the row is blank.

With auto-fit, empty tracks are collapsed to zero after the fitting step, and their gaps collapse with them. The 1fr tracks that remain share the whole row: (1000 − 16) ÷ 2 = 492px each.

Which is right depends on the design. A product grid usually wants auto-fill, so a search with two results shows two normal-sized cards instead of two giant ones. A row of three feature boxes that should always fill the width wants auto-fit.

Implicit tracks

Rows in a card grid are usually implicit: you didn’t define them, they were created because items needed somewhere to go. Their size comes from grid-auto-rows, which defaults to auto (as tall as the tallest item in the row). Setting grid-auto-rows: minmax(180px, auto) gives every row at least 180px and lets taller content grow it. And since every item in a row shares the row’s height, cards line up at the bottom without any equal-height tricks.

Interview practice

A dashboard uses grid-template-columns: repeat(3, 1fr) with a 24px gap inside a 960px container. One widget contains a 600px-wide chart with a fixed width. Describe the column widths you’ll see and how you’d change the CSS so the columns stay equal.

Answer

Without the chart, the free space is 960 − 48 = 912px, so each column would be 304px. But each 1fr is minmax(auto, 1fr), and the chart’s column has a min-content width of 600px, so that track’s minimum is 600px. The algorithm gives that column 600px, which leaves 960 − 48 − 600 = 312px for the other two, 156px each. The layout looks lopsided rather than overflowing, which can make it hard to spot.

Change the tracks to repeat(3, minmax(0, 1fr)) so each is exactly 304px regardless of content. Then make the chart fit its cell: give it max-width: 100% (with height: auto if it’s an image or SVG with a viewBox), or let the widget scroll with overflow-x: auto if the chart can’t scale. If the chart really needs more room, span it over two columns with grid-column: span 2 rather than letting its content dictate the track sizes.

Keep going

More in HTML & CSS

read ✓HTML & CSS · mid

CSS Container Queries for Reusable Components

Build a card that switches layout based on the space it's given, not the viewport: container-type, named containers, cqi units and the gotchas.

~6 min readread →
read ✓HTML & CSS · mid

CSS Responsive Units and Content Constraints

When to use px, rem, em, %, ch, vw and dvh, how clamp() builds fluid type that still zooms, and why 100vh breaks on phones, with real numbers.

~6 min readread →
esc