CSS Grid lays out the children of a container in rows and columns at the same time. Its rules are in CSS Grid Layout Module Level 2, a Candidate Recommendation Draft that keeps everything from Level 1 and adds subgrid. Most of the questions people search for, such as why a 1fr column refuses to shrink, why -1 does not reach the last column, or when auto-fill and auto-fit give different results, are answered in specific sections of that spec. This guide goes through them and checks each one in a browser. All measurements were taken on 2026-10-02 in Chrome 152 on macOS, by applying the declarations to test elements and reading getComputedStyle() and getBoundingClientRect().

Browser support is no longer a concern for the basics: display: grid works in Chrome 57, Firefox 52, Safari 10.1 and Edge 16 and later, and gap in grid containers from Chrome 66, Firefox 61 and Safari 12 (MDN browser-compat-data 8.1.4). Subgrid is newer and is covered in its own section.

Lines, tracks, cells and areas

Spec section 3 defines four words that the rest of the spec relies on:

  • Grid lines are the dividing lines. A grid with three columns has four vertical lines, numbered 1 to 4 from the start edge and −1 to −4 from the end edge.
  • Tracks are the rows and columns between two adjacent lines.
  • A cell is the intersection of one row and one column.
  • A grid area is any rectangle of one or more cells, bounded by four lines.

grid-template-columns and grid-template-rows list the tracks. With

.container {
  display: grid;
  width: 1000px;
  grid-template-columns: 200px 1fr 1fr;
}

Chrome resolved the columns to 200px 400px 400px, so the four column lines sit at 0, 200, 600 and 1000px. Items are placed by line numbers (grid-column: 2 / 4 covers the second and third columns), by names, or automatically in source order, one per cell.

Sizing tracks: fixed lengths, fr, auto and minmax()

A track size is one of a few kinds of value (§7.2.1):

ValueMeaning
240px, 20%, 15remA fixed size; percentages are of the container’s content box
1frA share of the free space left after the other tracks are sized
autoSized by the content, and stretched if there is space left over
min-content, max-contentThe smallest or largest size the content can take without overflowing
minmax(min, max)Anything between min and max
fit-content(300px)Like auto, but no larger than the argument

repeat(3, 1fr) is shorthand for 1fr 1fr 1fr. Gaps are subtracted before the fr shares are worked out, so in a 1000px container with repeat(3, 1fr) and a 16px column-gap, each column is (1000 − 2 × 16) ÷ 3 = 322.67px; Chrome reported 322.664px.

Rows behave less obviously, because the container usually has no fixed height. In a 600px-wide grid with grid-template-rows: repeat(2, 1fr) and six items, one of which was 200px tall and the rest 20px, Chrome gave both rows 200px: when the height is not definite, 1fr rows are sized from their content and then equalised to the largest. With repeat(2, auto) the same items gave 200px 20px. If you only want rows that fit their content, use auto, which is also what an unlisted (implicit) row gets.

Why a 1fr column grows past its share

Section 7.2.4 says that a flexible size outside minmax() “implies an automatic minimum (i.e. minmax(auto, <flex>))”. The automatic minimum of a grid item (§6.6) is roughly its min-content width: the longest word, URL, image or table it contains. Grid will not make the track smaller than that.

Two measurements in a 600px container with repeat(3, 1fr):

Content of the middle cellColumns in ChromeContainer scroll width
An unbreakable 500px element50px 500px 50px600px
An unbreakable 750px element9.20312px 750px 9.25px768px

In the first case the middle track takes 500px and the other two share the remaining 100px. In the second there is no free space left, the outer tracks shrink to their own content (one letter each), and the grid overflows its container by 168px. A long URL in a table cell or a <pre> block causes exactly this.

The fix is to give the tracks a zero minimum:

.container {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

With that, Chrome kept the columns at 200px 200px 200px. The 750px element still overflows its own cell (the container’s scroll width became 950px), so the content needs handling too: overflow-wrap: anywhere for text, max-width: 100% for images, or overflow: auto on a code block.

The explicit grid and the implicit grid

The tracks you list are the explicit grid (§7.1). When an item is placed outside it, or there are more items than cells, the browser adds implicit tracks (§7.5), sized by grid-auto-rows and grid-auto-columns (both auto by default). grid-auto-flow decides whether extra items create rows (the default) or columns, and dense lets later items fill earlier holes.

This matters for negative line numbers. Section 8.3 says a negative integer counts “starting from the end edge of the explicit grid”, so -1 is the last explicit line, not the last line of the whole grid. A test with three 200px explicit columns, grid-auto-columns: 100px, a header set to grid-column: 1 / -1 and another item set to grid-column: 5:

.container {
  display: grid;
  grid-template-columns: repeat(3, 200px);
  grid-auto-columns: 100px;
}
.header { grid-column: 1 / -1; }
.far    { grid-column: 5; }

Chrome resolved the columns to 200px 200px 200px 100px 100px. The far item sat at x = 700px in the fifth column, and the header covered only the first 600px. If a full-width element must also cover columns created later, give the grid enough explicit columns or place it with 1 / span 5.

Named lines and named areas

Line numbers break when a column is added. Names do not.

Named lines go in square brackets inside the track list (§7.2.2). A line can have several names:

.page {
  display: grid;
  grid-template-columns:
    [full-start] 1fr [content-start] 600px [content-end] 1fr [full-end];
}
.page > * { grid-column: content; }
.page > .wide { grid-column: full; }

grid-column: content works even though no area is called content: when lines named foo-start and foo-end exist, the grid has an implicitly named area foo (§7.3.3). In a 1000px container Chrome placed the normal children at x = 200px with a width of 600px, and .wide at x = 0 with the full 1000px. This is a common way to keep text in a readable column while letting images break out.

Named areas draw the layout as strings (§7.3). Each string is a row, each word a cell, and a . is an empty cell:

.app {
  display: grid;
  width: 960px;
  grid-template-columns: 240px 1fr;
  grid-template-rows: 60px 300px 40px;
  grid-template-areas:
    "head head"
    "side main"
    "foot foot";
  column-gap: 24px;
}
.app > header { grid-area: head; }
.app > nav    { grid-area: side; }
.app > main   { grid-area: main; }
.app > footer { grid-area: foot; }

Chrome put main at x = 264px, y = 60px, 696 × 300px, and the header and footer across the full 960px. The order of the elements in the HTML did not matter. Defining areas also defines lines: the column line before main gets the name main-start and the one after it main-end (§7.3.2), so grid-column: main-start / main-end lands in the same place. grid-area: main sets all four placement properties to main, and the browser resolves each of them to those implicit lines.

Two mistakes make the whole grid-template-areas declaration invalid, so the browser keeps whatever value applied before. Every string must have the same number of cells; a third row of just "foot" made Chrome compute none. And every area must be a rectangle; "a b" "b a" was rejected. The spec says non-rectangular or disconnected areas “may be permitted in a future version”.

auto-fill vs auto-fit

repeat() accepts auto-fill or auto-fit instead of a count, creating as many tracks as fit (§7.2.3.2). The count is the largest number of tracks that does not overflow the container, gaps included, using the minimum of a minmax() when the maximum is flexible. For a 1000px container, a 16px gap and minmax(180px, 1fr) that is floor((1000 + 16) ÷ (180 + 16)) = 5 columns.

The two keywords differ only in what happens to tracks with no items in them. With two items:

DeclarationColumns in ChromeSecond item
repeat(auto-fill, minmax(180px, 1fr))five columns of 187.2px (three empty)x = 203.2px, 187.2px wide
repeat(auto-fit, minmax(180px, 1fr))492px 492px 0px 0px 0pxx = 508px, 492px wide

With auto-fill the empty columns keep their space, so two items stay card-sized and leave room on the right. With auto-fit the empty columns collapse to zero (their gaps collapse too) and the two items stretch across the row. Once there are enough items to fill every column, the two give the same result.

Neither keyword helps when the container is narrower than the minimum. In a 150px container, minmax(180px, 1fr) produced one 180px column and a 30px overflow. Writing the minimum as minmax(min(180px, 100%), 1fr) gave one 150px column instead; min() works in Chrome 79, Firefox 75 and Safari 11.1 and later.

Subgrid: aligning rows across cards

A grid item can be a grid container itself, but its tracks are normally independent of its parent’s. That is why the footers of a row of cards do not line up when the titles have different lengths. Level 2 adds subgrid (§9): an item that spans several tracks of its parent can use those tracks as its own.

.cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  column-gap: 20px;
}
.card {
  display: grid;
  grid-row: span 3;               /* title, body, footer */
  grid-template-rows: subgrid;
}

In a test with three cards whose titles were 24, 72 and 48px tall, bodies 80, 80 and 120px, and 30px footers, the footers started at y = 104, 152 and 168px when each card had its own auto auto auto rows. With grid-template-rows: subgrid all three footers started at y = 192px, and the parent’s rows resolved to 72px 120px 30px: each shared row became as tall as the tallest entry from any card.

Subgrid is supported in Chrome and Edge 117, Firefox 71 and Safari 16 and later (MDN browser-compat-data 8.1.4). In older browsers grid-template-rows: subgrid is an invalid value and is ignored, so the cards fall back to their own rows and still work, with unaligned footers.

Grid or flexbox

Grid sizes rows and columns together: a cell’s width depends on the column it is in, not only on its own content. Flexbox sizes each line independently, so items in different rows do not line up unless they have fixed widths. Use grid when things must align in two directions (page layouts, card grids, forms with a label column) and flexbox for a single row or column whose items should keep their own sizes (navigation bars, button groups, a row of tags). The two combine well: a grid card can use flexbox inside it. The CSS flexbox guide covers the other side.

Building the container with the generator

The CSS grid generator writes the container rule only. It has number fields for Columns (1–12) and Rows (1–8), text fields for the column and row gaps, and two free-text fields for grid-template-columns and grid-template-rows. Changing a count rewrites the matching template field to repeat(N, 1fr), so set the counts first and type a custom template last. On load it shows:

.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(2, 1fr);
  column-gap: 1rem;
  row-gap: 1rem;
}

As shown above, those 1fr rows make every row as tall as the tallest one. For the overflow-safe tile grid from the auto-fill section, set Rows to 1, type the template, and set the gaps:

.container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(180px, 100%), 1fr));
  grid-template-rows: auto;
  column-gap: 16px;
  row-gap: 16px;
}

Named lines can be typed into the template field as well; with Columns at 3 and Rows at 1:

.container {
  display: grid;
  grid-template-columns: [full-start] 1fr [content-start] 600px [content-end] 1fr [full-end];
  grid-template-rows: auto;
  column-gap: 0;
  row-gap: 0;
}

Keep in mind what the tool does not do. It has no fields for grid-template-areas, item placement (grid-column, grid-area), alignment or subgrid; add those to the copied CSS. The preview always draws Columns × Rows cells, so with an auto-fill template the extra cells flow into rows just as real children would. The template and gap fields are copied as typed without validation: an invalid value such as repeat(3 1fr) ends up in the output, and the preview keeps the last valid template because the browser ignores the invalid one.