CSS from the ground up

Grid

Two dimensions at once: columns that tile on their own, rows that line up, and no widths to calculate.

Part 7 of 12

Intermediate15 min readBack to Courses

Columns, declared

Flexbox lays things along one line. Grid lays them across two. display: grid on a parent turns its children into cells, and grid-template-columns says how many columns exist and how wide they are.

The magic line for a card layout is repeat(auto-fill, minmax(16rem, 1fr)): as many columns as fit, each at least 16rem wide, sharing the leftovers evenly. No media queries, no breakpoints — drag the window and the grid re-tiles itself.

grid-template-columns describes the columns; the rows are created as items arrive. That is why a grid with three columns and seven cards makes three rows without you writing a row rule, and why the last row can look different when it holds only one card. If the rows need a consistent height, grid-auto-rows is the property that sets it.

A grid that tiles itself

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

Gap belongs to the grid

One gap spaces both directions — the rows and the columns — which is why grid layouts are so quiet: no child needs a margin, and the ends stay flush with the container.

align-items: start stops cards stretching to the tallest one in their row when you do not want that, and place-items: center centres everything in its cell. Both are one line instead of a trick.

Grid or flex?

The rule of thumb: use flexbox for a line of things — a navbar, a toolbar, a row of buttons. Use grid when the layout has rows and columns at the same time — a page skeleton, a card gallery, a form with labels beside its fields.

They mix happily: a grid of cards, each card a flex column whose button sits at the bottom. Choose per container, not per project.

Tracks and lines

grid-template-columns declares tracks, and the spaces between them are lines. A three-column grid has four vertical lines, numbered from 1 on the left; -1 is the last line, which is why grid-column: 1 / -1 means “all the way across” without counting.

Items are placed into cells in source order unless you place them yourself. If there are more items than the declared rows, the grid creates implicit rows — real rows, sized by grid-auto-rows rather than by anything you wrote. Giving that property a minmax() keeps the extra rows from collapsing.

Placing an item by line

Most items are placed automatically, in source order, one cell at a time. When one needs to sit somewhere specific, name its lines: grid-column: 2 / 4 starts at vertical line 2 and ends at line 4, which is two columns wide. grid-row does the same across rows, and the two together are what grid-area abbreviates.

A line number is a position between tracks, not a track. A three-column grid has four vertical lines, so 1 / -1 means from the first to the last. Counting from the end with negative numbers is how a rule survives a change from three columns to four: -1 is still the last line, while 4 would now point somewhere else.

An item placed this way is taken out of the automatic flow, and the cells it would have occupied are left for the others — which is how a banner can cross a page without pushing the cards below it out of their columns. If two placed items land on the same cell, the later one simply paints over the earlier, so overlap is a decision rather than an accident.

auto-fill, auto-fit, and the fr floor

repeat(auto-fill, minmax(16rem, 1fr)) asks for as many tracks as fit. auto-fill keeps the empty tracks it counted, so a short row still leaves gaps; auto-fit collapses the empty ones and lets the remaining columns stretch. The difference shows when there are fewer items than columns.

1fr also has an automatic minimum: a track will not shrink below its longest word. minmax(0, 1fr) removes that floor, which is what you want for a column of long strings or inputs. The grid is doing the arithmetic; the minimum is the one number it will not guess.

Three grid mistakes: writing grid-template-columns: 1fr 1fr and expecting it to stack on a phone — both tracks always exist; using a bare 1fr for a column of long words and watching the whole grid widen; and putting margins on the cards when the grid already has gap, which doubles the space at every seam.

Try these next

  • Make the first card span every column with grid-column: 1 / -1.
  • Put two cards in a wide grid, compare auto-fill with auto-fit, and find the empty track.
  • Add grid-auto-rows: minmax(6rem, auto) and a fourth card, then watch the implicit row appear.

A gallery that tiles

The three cards are stacked blocks. Lay them out as a grid of columns at least 16rem wide that fill the row, with an even gap between them.

Live Preview
Console output will appear here...

3 left to do

  • The cards are laid out with grid
  • The columns are declared
  • The cells have a gap

display: grid; then grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); and gap: 1rem;.

A page skeleton with a banner

Lay the page out as a grid: a header and a footer that span the full width, and a main column beside a sidebar. Declare the columns once and let the two middle blocks share the row.

Live Preview
Console output will appear here...

3 left to do

  • The layout is a grid
  • The two columns are declared
  • The banner spans every column

grid-template-columns: 2fr 1fr; on .layout, then grid-column: 1 / -1; on the header and the footer.

minmax(16rem, 1fr) is the whole responsive grid most sites need: a minimum width below which a column is too cramped, and a share of the leftover space above it.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

When is grid the better tool than flexbox?