Skip to content

Grid

docs.scrimba.com

You have a page with a header across the top, a sidebar down the left, a main content area, and a footer. You could nest a stack of containers and nudge each one into place, and it would mostly work until the day you need to change the arrangement. CSS grid was built for exactly this shape of problem: laying out a page in rows and columns at the same time. Once you can describe a layout as a set of tracks, most of the fiddling with wrappers and floats falls away.

The grid container

You turn any element into a grid by giving it display: grid. That element becomes the grid container, and its direct children become items that snap into a grid you define. On its own, display: grid does not do much, so you also tell it how many columns and rows you want and how big each should be.

You describe the columns with grid-template-columns and the rows with grid-template-rows. Each value in the list is one track, and a track is a single column or row:

css
.layout {
  display: grid;
  grid-template-columns: 200px 1fr;   /* two columns: a fixed one, then the rest */
  grid-template-rows: 80px 400px;     /* two rows: a short one, then a tall one */
}

Setting display: grid on an element makes it a grid container, and its direct children become grid items laid onto a grid you define. This is the two-dimensional counterpart to flexbox, which lays items out along a single axis at a time. Flexbox asks "how should this row (or column) of items share space?"; grid asks "where do these items sit in a set of rows and columns?".

You define the tracks with grid-template-columns and grid-template-rows. Each value is one track, a column or a row, and the values can mix units: fixed lengths, percentages, or the flexible fr unit you meet in the next section.

css
.layout {
  display: grid;
  grid-template-columns: 200px 1fr;   /* fixed sidebar, flexible main column */
  grid-template-rows: auto 1fr auto;  /* header sizes to content, main fills, footer to content */
}

display: grid establishes a grid formatting context on the element: it becomes a grid container, its direct children become grid items, and the normal-flow rules that governed those children no longer apply. Text nodes are wrapped in anonymous items, and properties like float and vertical-align stop having any effect on the items, because their placement is now the grid's job.

The distinction from flexbox is dimensionality, and it is the first decision to get right. Flexbox is one-dimensional: it distributes items along a main axis and wraps as a side effect, but it does not align content into a matching grid on the cross axis. Grid is two-dimensional: it defines rows and columns together, so an item's row position and column position are set independently and items line up on both axes at once. You define the explicit tracks with grid-template-columns and grid-template-rows, each value producing one track:

css
.layout {
  display: grid;
  grid-template-columns: 200px 1fr;   /* two explicit column tracks */
  grid-template-rows: auto 1fr auto;  /* content, fill, content */
}

The auto keyword sizes a track to its content; 1fr absorbs the leftover space, which is the mechanism the next section builds on.

JunoThe grid container Add display: grid to an element and it becomes a grid container, with its direct children sitting inside as items. Then you say how many columns and rows you want with grid-template-columns and grid-template-rows, one value per track. Nothing lines up until you define those tracks, so that is always your first move.
JunoThe grid container Flexbox works along one axis at a time; grid works in rows and columns together, which is why page layouts feel more natural in grid. Set display: grid, then list your tracks in grid-template-columns and grid-template-rows. Reach for grid when you are placing things in a two-dimensional arrangement, and flexbox when you are lining up one row or column.
JunoThe grid containerdisplay: grid makes a grid formatting context, so the children stop obeying normal flow and float and vertical-align quietly stop mattering on them. The real split from flexbox is dimensionality: flex distributes along one axis, grid places on both at once. Pick grid when row and column position are both meaningful, flex when only one axis is.

Flexible tracks

Fixed pixel columns are fine, but most layouts want columns that share the space evenly and grow with the screen. That is what the fr unit is for. One fr means one share of the leftover space, so three columns of 1fr each take a third of the container:

css
.gallery {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;   /* three equal columns */
}

Typing 1fr three times gets tedious, so repeat() writes it for you. And gap sets the space between the tracks, so you do not reach for margins on every item:

css
.gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);   /* the same three equal columns */
  gap: 16px;                               /* space between rows and columns */
}

The fr unit measures a share of the free space in the container after fixed sizes and gaps are taken out. Three columns at 1fr each get an equal third; a 2fr 1fr pair splits the space two-to-one. Because fr divides what is left, columns stay proportional as the container grows or shrinks, which fixed lengths cannot do.

Two helpers make track lists readable. repeat(3, 1fr) expands to 1fr 1fr 1fr, and gap sets the gutters between tracks in one property (with row-gap and column-gap when you want them different):

css
.gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);   /* three equal columns */
  gap: 16px;                               /* 16px gutters, rows and columns */
}

Gutters are set with gap, not margins on the items. Because gap only applies between tracks and never on the outer edges, the grid sits flush to its container while the items stay evenly spaced.

The fr unit is a flexible length: it represents a fraction of the container's free space, the room that remains after fixed track sizes, content-sized tracks, and gaps are resolved. A 2fr 1fr 1fr track list hands the first column half the free space and the other two a quarter each. The subtlety is that fr distributes free space, not total width, so a track holding wide content can push past its fr share unless you cap it, which is where minmax() in the last section comes in.

repeat() is a compact way to write a run of tracks: repeat(3, 1fr) is 1fr 1fr 1fr, and it composes, so repeat(2, 200px 1fr) yields four tracks. gap (the modern name for grid-gap) sets gutters between tracks only, never around the outside of the grid, so it does not fight with the container's own padding.

css
.gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);   /* three equal columns */
  gap: 16px 24px;                          /* 16px row gap, 24px column gap */
}

Note the two-value gap: row gap first, then column gap. A single value applies to both.

JunoFlexible tracks One fr is one share of the leftover space, so 1fr 1fr 1fr gives you three equal columns. Use repeat(3, 1fr) so you are not typing the same value over and over, and gap for the space between tracks. Once fr clicked for me, I stopped reaching for percentages that never quite added up.
JunoFlexible tracksfr splits the free space in the container, so proportional columns stay proportional as the page resizes. Lean on repeat() to keep track lists short and gap for gutters instead of per-item margins. Remember gap only sits between tracks, never on the outer edge, which is usually exactly what you want.
JunoFlexible tracksfr divides free space, not total width, so a track with wide content can overflow its share until you cap it with minmax(). repeat() composes, so repeat(2, 200px 1fr) is four tracks. And two-value gap reads row gap first, then column gap, which trips people who assume it matches the clockwise shorthand.

Placing items

By default each item drops into the next free cell, one per cell, in order. When you want an item to take up more than one cell, you say so with grid-column and grid-row. The lines between tracks are numbered starting at 1, so grid-column: 1 / 3 means "start at line 1, end at line 3", covering two columns:

css
.featured {
  grid-column: 1 / 3;   /* span from column line 1 to line 3, two columns wide */
}

If you do not want to count lines, span says how many cells to cover from wherever the item lands:

css
.featured {
  grid-column: span 2;   /* cover two columns, starting wherever this item sits */
}

Grid placement is described with grid lines, the numbered lines that sit between and around the tracks. A three-column grid has four column lines, 1 through 4. grid-column: 1 / 3 places an item from line 1 to line 3, so it spans the first two columns; grid-row does the same vertically. The span keyword avoids counting: grid-column: span 2 covers two columns from wherever the item is placed.

For a whole-page layout, naming the regions reads better than counting lines. grid-template-areas lets you draw the layout as a set of named cells, then assign each item to a name:

css
.page {
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-template-areas:
    "sidebar header"
    "sidebar main";     /* sidebar spans both rows by repeating its name */
}

.page > header { grid-area: header; }
.page > aside  { grid-area: sidebar; }
.page > main   { grid-area: main; }

Placement runs on grid lines, the numbered boundaries around every track (an n-track axis has n + 1 lines). grid-column: 1 / 3 sets the start and end column line, spanning the tracks between them; grid-column: span 2 sets a span without pinning a start, letting the auto-placement algorithm choose where it lands. Negative line numbers count from the end, so grid-column: 1 / -1 always spans the full width regardless of column count, which is the durable way to write a full-bleed row.

grid-template-areas names cells so you place items by name rather than by line number. Each string is a row, each token a cell, and repeating a token across cells makes one item span them. The whitespace-formatted block doubles as a readable diagram of the layout:

css
.page {
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "sidebar header"
    "sidebar main";     /* sidebar occupies both rows of column one */
}

.page > aside { grid-area: sidebar; }

Two rules keep it honest: every row string must have the same number of cells, and the cells for one name must form a solid rectangle, or the declaration is invalid and ignored. A . token marks an empty cell you want to leave blank.

JunoPlacing items Items fill one cell each by default, and you make one stretch across cells with grid-column or grid-row. Line numbers start at 1, so grid-column: 1 / 3 covers two columns, or use span 2 if you would rather not count. That is enough to build most layouts before you ever name a single area.
JunoPlacing items Place items with grid lines: grid-column: 1 / 3 spans two columns, and span 2 does it without counting from a fixed start. For a full page, grid-template-areas lets you name the regions and read the layout at a glance, which beats tracking line numbers by hand. Repeat a name across cells and that region spans them.
JunoPlacing items Lines can be negative, so grid-column: 1 / -1 spans the full width no matter how many columns there are, which is the reliable full-bleed trick. With grid-template-areas, every row string needs equal cells and each named region must be a solid rectangle, or the whole declaration is dropped. Use a . token to leave a cell deliberately empty.

Responsive grids without media queries

A row of cards should show three across on a wide screen, two on a tablet, one on a phone. You can do that with grid alone, no breakpoints to write. The trick is one line:

css
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
}

Read it inside out. minmax(200px, 1fr) means each column is at least 200px and at most one share of the space. auto-fit then fits as many of those columns as the container has room for. Wide screen, more columns fit; narrow screen, fewer fit, and the cards reflow on their own.

The pattern that reflows cards without a single media query is repeat(auto-fit, minmax(200px, 1fr)). Each piece does one job. minmax(200px, 1fr) defines a track that is never smaller than 200px and never larger than one fr share. auto-fit repeats that track as many times as fit across the current width, so the column count responds to the container instead of to fixed breakpoints.

css
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
}

The 200px floor is your real control: it is the narrowest a card is allowed to get before the grid drops to fewer columns. This pairs naturally with the fluid techniques in responsive design, and it usually replaces the media queries you would otherwise write for a card grid.

repeat(auto-fit, minmax(200px, 1fr)) is the canonical responsive-grid idiom, and each part earns its place. minmax(min, max) defines a track with a lower and upper bound: minmax(200px, 1fr) never shrinks below 200px and never grows past one free-space share. The 200px floor sets the reflow threshold, the width at which the grid gains or loses a column.

The choice between auto-fit and auto-fill is the part worth knowing. Both compute how many minmax() tracks fit the container. auto-fill keeps the leftover empty tracks in the grid, so a half-empty row of cards stays at its column width. auto-fit collapses those empty tracks to zero width, so the present items expand to fill the row:

css
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));   /* items stretch to fill */
  gap: 16px;
}

Watch one edge: minmax(200px, 1fr) can overflow a container narrower than 200px plus gaps, because the floor is a hard minimum. minmax(min(200px, 100%), 1fr) guards against that by letting the floor drop to the container width on very small screens. For sizing the floor reliably across devices, sizing units covers how the length units resolve.

JunoResponsive grids without media queriesrepeat(auto-fit, minmax(200px, 1fr)) makes cards reflow on their own, no breakpoints needed. Each column is at least 200px and at most one share of the space, and auto-fit packs in as many as fit. Change the 200px and you change how many cards sit side by side.
JunoResponsive grids without media queries One line, repeat(auto-fit, minmax(200px, 1fr)), replaces a stack of card-grid media queries. The 200px floor is the lever: it decides how narrow a card gets before the grid drops a column. Reach for this before you write breakpoints for anything that is really a reflowing grid of cards.
JunoResponsive grids without media queriesauto-fit collapses empty tracks so present items stretch to fill; auto-fill keeps the empty tracks and leaves gaps, which is the whole difference between them. Watch the overflow case: a hard 200px floor can spill on very narrow containers, so minmax(min(200px, 100%), 1fr) is the safer floor. The floor is what sets your reflow points, so choose it with the smallest sensible card in mind.

Explicit and implicit grids

Everything so far defines the explicit grid: the tracks you name in grid-template-columns and grid-template-rows. When items land outside that defined grid, the browser creates tracks to hold them, and those form the implicit grid. Add a fourth item to a three-column, one-row explicit grid and it starts a new, implicitly created second row. Implicit tracks size themselves auto by default, which is why an implicit row is only as tall as its content unless you say otherwise.

grid-auto-rows sets the size of those implicit rows (and grid-auto-columns the implicit columns), so a card grid whose row count you cannot predict still gets even rows:

css
.gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);   /* explicit: three columns */
  grid-auto-rows: 240px;                   /* implicit rows are 240px each */
  gap: 16px;
}

Alignment then decides where items and tracks sit within their cells. justify-items aligns items along the row (inline) axis and align-items along the column (block) axis, each accepting start, end, center, or stretch (the default, which fills the cell). place-items is the shorthand for both, block axis first:

css
.gallery {
  place-items: center;   /* align-items and justify-items, both centered */
}

minmax() also takes the content keywords min-content and max-content: min-content is the smallest a track can be without overflowing (roughly the longest unbreakable word), and max-content is the size the content wants when nothing forces it to wrap. A track like minmax(min-content, max-content) sizes to content within those bounds. And subgrid lets a nested grid inherit its parent's track lines, so a child grid's columns line up with the outer grid instead of defining their own.

On the grid-versus-flex decision, the clean rule is this. If you are arranging content on one axis and letting it wrap as a consequence, use flexbox: a nav bar, a row of tags, a toolbar. If you are placing content into rows and columns that must line up on both axes, use grid: a page shell, a card gallery, a form with aligned labels and fields. They compose well, so a grid cell often holds a flex container, and that combination covers most layouts you will build.

JunoExplicit and implicit grids The tracks you declare are the explicit grid; anything the browser adds to hold overflowing items is the implicit grid, and those extra tracks are auto-sized until grid-auto-rows or grid-auto-columns says otherwise. Alignment lives on justify-items and align-items, with place-items as the shorthand and stretch as the default. The decision is one axis with wrapping means flexbox, two axes that must line up means grid, and they nest happily.