A little help goes a long way. Find a tool/

Design FIELD NOTES · 4 MIN READ

CSS Grid: Tracks, fr Units, and Gaps

How grid-template-columns and rows define the skeleton: track sizes, the fr unit, repeat(), minmax(), and gap.

Grid looks intimidating because its grammar is dense — repeat(auto-fit, minmax(180px, 1fr)) — but the grammar has exactly three moving parts: how many tracks, how wide each one is, and how far apart they sit. Learn those three and every grid template on the web reads like a sentence.

Tracks: the skeleton of the layout

grid-template-columns and grid-template-rows define tracks — the columns and rows items snap into. Each value in the space-separated list is one track, and the list order is the layout order: grid-template-columns: 200px 1fr auto means a fixed 200px first column, a flexible second, and a third sized to whatever it contains. That trio is the sidebar-content-metadata layout, written in one line.

Track sizes come in four flavors. Fixed units (200px, 12rem) never budge. Percentages (30%) are shares of the container but overflow when summed past 100% once gaps exist. Keywords (auto, min-content, max-content) size to the content. And fr — the one that makes Grid feel magic — distributes the leftover space.

The fr unit: shares of what is left

One fr is one share of the free space remaining after fixed tracks and gaps are paid for. In 200px 1fr 2fr on a 1000px container, the 200px column goes first, then the remaining 800px splits 1:2 — so 266.67px and 533.33px. Crucially, repeat(3, 1fr) always fits the container exactly, while 30% 30% 30% overflows before any gap is added. That difference is why fr is the default choice for anything flexible and percentages are the exception.

display: grid;
grid-template-columns: 200px 1fr 2fr;   /* 200px, then 1:2 of the rest */

repeat() and minmax(): the patterns worth memorizing

repeat(3, 1fr) is shorthand for 1fr 1fr 1fr — use it whenever tracks are equal, because the count is visible at a glance. The function accepts patterns too: repeat(2, 200px 1fr) tiles a two-track pattern. The combination that ends most responsive stylesheets is:

grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));

Read it inside-out: each column is at least 180px wide but may grow to an equal share (1fr) of the leftover space; auto-fit means "fit as many as possible". The result is a card grid that adds columns as the screen widens — no media queries. Its sibling auto-fill is identical except it keeps empty tracks alive when items run out, holding the layout's column count stable.

Implicit rows: the ones you didn't write

Card layouts usually declare only the columns. When items exceed the explicit rows — or when no rows are declared at all — the grid creates implicit rows sized to their content. That is the "leave rows empty" advice in practice: content-driven row heights are usually what you want, and grid-auto-rows styles the implicit ones when a minimum matters, e.g. minmax(100px, auto). Declare explicit rows only when the vertical rhythm is part of the design: repeat(2, 200px) for fixed-height rows, or a single auto plus implicit rows for hybrid layouts.

gap: the spacing that replaced the hacks

gap spaces tracks on both axes with one declaration — gap: 12px, or gap: 12px 24px for separate column and row gaps. It only sits between tracks: nothing is added before the first column or after the last row, so containers stay flush with their parents without negative-margin cleanup. That edge behavior is exactly what the old margin + :nth-child workflows tried to fake, and gap does it while surviving any change to the item count.

One nuance worth knowing: gap participates in the fr calculation before fr tracks divide the leftovers, so repeat(3, 1fr) with a 12px gap yields three equal columns each 12px narrower than the no-gap case — the gaps are paid first, the shares divide what remains.

Try it yourself

The CSS Grid Generator accepts either a plain track count or a full track list, validates every token against the real CSS grammar, and emits the declaration block with a live preview of the tracks. Pair it with the CSS Flexbox Generator for one-directional flows inside each cell, and the CSS Unit Converter when a design handoff swaps px for rem.

Frequently asked questions

How is fr different from %?
A percentage is a share of the container's full width, so 30% + 30% + 30% overflows before gaps are even added. fr is a share of the space left after fixed tracks and gaps, so repeat(3, 1fr) always fits exactly — that is why fr is the default choice for flexible tracks.
What are implicit rows?
Rows you did not declare but the grid creates anyway when items run out of explicit tracks. They size to their content by default, and grid-auto-rows styles them. Card layouts usually rely on implicit rows and only declare the columns.
What does repeat(auto-fit, minmax(180px, 1fr)) do?
It packs in as many columns as fit while each stays at least 180px wide, then stretches them equally — a responsive card grid with no media query. Auto-fill is the same but keeps empty tracks, holding columns open when items run out.
Does gap collapse at the container edges?
No — gap only sits between tracks, never before the first or after the last. That edge behavior is exactly what padding on the container is for, and it is why gap replaced the margin-plus-negative-margin hacks of the float era.