HTML & CSS NotesRohit’s interview study guide
Chapter 07

Grid

CSS Grid as a two-dimensional layout system: lines, tracks and areas, the fr unit, repeat() and minmax(), placing items, template areas, the implicit grid, alignment, subgrid, and when to pick grid over flexbox.

9 topics
Parts marked Advanced are extra depth. Skip them on a first read or a quick revision.

#The grid model: lines, tracks, cells and areas

Added

CSS Grid lays items out in rows and columns at the same time. You declare display: grid on a parent (the grid container), describe its columns and rows, and its direct children (the grid items) are placed into the result. Flexbox, by contrast, lays items out along one line at a time (see The flex model: main axis and cross axis); grid is the tool when you want things to line up in both directions.

The mental model is a sheet of graph paper with numbered lines. The lines are the real coordinates: a grid with three columns has four column lines, numbered 1 to 4 from the start edge, and also -1 to -4 from the end edge. The space between two neighbouring lines is a track (a column or a row). Where a column track crosses a row track you get a cell, the smallest unit. A rectangle of one or more cells is a grid area, and every item occupies exactly one area. A gap is a gutter between tracks: it isn't a track, and nothing can be placed in it.

TermWhat it isIn the example below
Grid lineA dividing line, numbered from 1 (or from -1 at the end)4 column lines, 3 row lines
TrackThe space between two adjacent lines: a column or a row3 columns of 100px, 2 rows of 50px
CellOne column crossed with one row6 cells
Grid areaA rectangle of cells, bounded by four linesB covers 2 cells, column lines 2 to 4
Gap (gutter)Space between tracks, not a track10px between every track
Three columns, two rows, one item spanning two cells
CSS
.grid {
  display: grid;
  grid-template-columns: 100px 100px 100px;
  grid-template-rows: 50px 50px;
  gap: 10px;
}
.grid > div { background: #dbeafe; color: #1e3a8a; padding: 4px 8px; }
.grid > .b { grid-column: 2 / 4; background: #fde68a; color: #78350f; }
HTML
<div class="grid">
  <div class="a">A</div>
  <div class="b">B: lines 2 to 4</div>
  <div class="c">C</div>
  <div class="d">D</div>
  <div class="e">E</div>
</div>
Resultlive, in your browser
What’s happening
  1. display: grid makes .grid a grid container, and its five child divs become grid items. Only direct children are items: anything nested inside them is laid out normally inside its item.
  2. grid-template-columns: 100px 100px 100px creates three column tracks, and therefore column lines 1, 2, 3 and 4. grid-template-rows: 50px 50px creates two row tracks and row lines 1 to 3.
  3. gap: 10px puts a 10px gutter between neighbouring tracks in both directions, so the column lines sit at 0, 110, 220 and 320px from the container's left edge (line 2 and line 3 are "thick": a line plus its gutter).
  4. A has no placement, so auto-placement puts it in the first free cell: column 1, row 1, measured at 0, 0 with size 100 × 50.
  5. B says grid-column: 2 / 4: start at column line 2, end at column line 4. Its area covers two cells and the gap between them, measured 210px wide (100 + 10 + 100) starting at 110px. Its row isn't set, so auto-placement keeps it in row 1, where columns 2 and 3 are still free.
  6. Row 1 is now full, so C, D and E flow into row 2 at 0, 110 and 220px, starting 60px down (50px row plus 10px gap). That's the whole model: lines are coordinates, tracks are the spaces between them, and every item gets a rectangle of cells.
AdvancedPitfall: negative lines and implicit tracks
AdvancedOnly direct children are grid items

A grid container's grid only applies to its children. A grandchild with grid-column set does nothing, because its parent isn't a grid. That surprises people who wrap items in an extra div (a link, a <li>, a component's root) and then wonder why placement stopped working. display: contents on the wrapper removes the wrapper's own box, so its children are promoted to items of the outer grid. Subgrid is the other answer, when the wrapper must keep its box.

A wrapper hides its children from the grid, display: contents brings them back
CSS
.grid {
  display: grid;
  grid-template-columns: 80px 80px 80px;
  gap: 10px;
  margin-bottom: 12px;
}
.grid div { height: 30px; background: #dcfce7; color: #166534; font-size: 13px; }
.grid .wrap { height: auto; background: #fef3c7; }
.grid .late { grid-column: 3; background: #fbcfe8; color: #831843; }
.contents .wrap { display: contents; }
HTML
<div class="grid plain">
  <div class="wrap"><div>1</div><div class="late">2: col 3?</div></div>
</div>
<div class="grid contents">
  <div class="wrap"><div>1</div><div class="late">2: col 3</div></div>
</div>
Resultlive, in your browser
What’s happening
  1. In the first grid, the only grid item is .wrap. It gets column 1 (80px wide, measured). Its two children are ordinary blocks stacked inside it.
  2. .late has grid-column: 3, but its parent .wrap isn't a grid container, so the property doesn't apply. It sits at 0px, inside the wrapper, in column 1.
  3. In the second grid, .wrap has display: contents. Its box disappears and its children become the grid's items: 1 goes to column 1 and .late goes to column 3, measured 180px from the left (80 + 10 + 80 + 10).
  4. display: contents has a cost: the wrapper has no box, so its own background, border and padding are gone. Use it on pure wrappers (a <ul> around <li>s is the classic case) and check that the semantics you need survive in the browsers you support.
Count the lines

A grid has grid-template-columns: repeat(4, 1fr) and grid-template-rows: 100px 100px 100px. How many column lines and row lines are there, and what is line -1 in each direction?

Show answer

Five column lines and four row lines. Column line -1 is line 5, row line -1 is line 4.

What’s happening
  1. Lines bound tracks, so there's always one more line than tracks: 4 columns → lines 1 to 5, 3 rows → lines 1 to 4.
  2. Negative numbers count backwards from the end of the explicit grid: -1 is the last line (5 for columns, 4 for rows), -2 is the one before it, and so on.
  3. So grid-column: 1 / -1 means 1 / 5, which spans all four columns. Gaps never add lines; they only widen the existing ones.

#Defining tracks: grid-template-columns and the fr unit

Added

grid-template-columns and grid-template-rows take a track list: one size per track, separated by spaces. Each size can be a length (100px, 10rem), a percentage of the container, a keyword based on content (auto, min-content, max-content, fit-content(200px)), a range (minmax(100px, 1fr)), or a flexible length in fr.

fr is a share of the leftover space. The browser first sizes everything that isn't flexible (fixed tracks, content-sized tracks, and the gaps), subtracts that from the container's size, and divides what remains between the fr tracks in proportion to their numbers. 1fr 2fr means "split the leftover into three parts; the first track gets one, the second gets two". That's why fr works where percentages don't: percentages ignore the gaps and overflow, fr takes the gaps out first.

Track sizeMeans
200px, 10remExactly that size
25%A share of the container's size (gaps not subtracted)
1fr, 2frA share of the space left after fixed tracks and gaps; never smaller than the content's minimum (see below)
autoSized to the content; grows to fill the container if no fr track is there to take the space
min-content / max-contentThe content's narrowest (every soft wrap taken) / widest (no wrapping) size
fit-content(300px)Like auto, but stops growing at 300px
minmax(a, b)At least a, at most b
100px 1fr 2fr in a 600px container
CSS
.grid {
  display: grid;
  width: 600px;
  grid-template-columns: 100px 1fr 2fr;
  gap: 20px;
  background: #f1f5f9;
}
.grid > div { height: 40px; background: #dbeafe; color: #1e3a8a; padding: 0 6px; }
.grid > div:first-child { background: #e2e8f0; color: #334155; }
HTML
<div class="grid">
  <div>100px</div>
  <div>1fr</div>
  <div>2fr</div>
</div>
Resultlive, in your browser
What’s happening
  1. The container is 600px wide. The browser takes out the parts that aren't flexible first: the 100px track and two gaps of 20px, which leaves 600 − 100 − 40 = 460px.
  2. The flexible tracks ask for 1fr + 2fr = 3fr in total, so one fr is 460 / 3 = 153.33px.
  3. The second track gets 1fr → measured 153.3px; the third gets 2fr → 306.7px. getComputedStyle reports the resolved track list as "100px 153.328px 306.672px", not 153.333: Chrome lays out in units of 1/64 px, so a third of a pixel is rounded to the nearest 1/64 and the remainder goes to the last track. The total is still exactly 460px.
  4. Everything adds up to exactly 600px, gaps included. With percentages you would have to do this arithmetic yourself, and the next example shows what happens when you don't.
AdvancedPercentages don't know about gaps

25% 25% 25% 25% looks like "four equal columns", but each percentage is a quarter of the whole container, and the gaps are added on top. The tracks overflow by the total gap width. repeat(4, 1fr) divides what's left after the gaps.

Four quarters plus gaps overflow, four fr tracks don't
CSS
.grid {
  display: grid;
  width: 400px;
  gap: 10px;
  margin-bottom: 10px;
  outline: 2px dashed #94a3b8;
}
.pct { grid-template-columns: 25% 25% 25% 25%; }
.fr { grid-template-columns: repeat(4, 1fr); }
.grid > div { height: 30px; background: #fee2e2; color: #7f1d1d; }
.fr > div { background: #dcfce7; color: #166534; }
HTML
<div class="grid pct"><div>25%</div><div>25%</div><div>25%</div><div>25%</div></div>
<div class="grid fr"><div>1fr</div><div>1fr</div><div>1fr</div><div>1fr</div></div>
Resultlive, in your browser
What’s happening
  1. Both containers are 400px wide with three gaps of 10px between four tracks.
  2. In .pct, each 25% is a quarter of 400px = 100px (measured). Four of those plus 30px of gaps is 430px: the last track ends 30px outside the dashed outline.
  3. In .fr, the gaps come out first: (400 − 30) / 4 = 92.5px per track, and the last one ends exactly at 400px.
  4. Use fr for "share the space" and keep percentages for when you really mean a fraction of the container regardless of gaps.
AdvancedWhy a 1fr column won't shrink: the automatic minimum

1fr is short for minmax(auto, 1fr), and the auto minimum means "never narrower than the content's min-content width". A column holding something that can't wrap (a long URL, a <pre>, a wide image, a table, a fixed-width widget) refuses to shrink below it, and the other fr columns are squeezed instead. This is the grid version of the flexbox min-width: auto problem (see Why flex items overflow: min-width auto), and the fix is the same idea: allow a zero minimum with minmax(0, 1fr).

A 200px-wide child inside three 1fr columns
CSS
.grid {
  display: grid;
  width: 300px;
  margin-bottom: 10px;
  outline: 2px dashed #94a3b8;
}
.auto-min { grid-template-columns: 1fr 1fr 1fr; }
.zero-min { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid > div { height: 34px; background: #e0e7ff; color: #312e81; }
.grid > div:nth-child(2) { background: #c7d2fe; }
.wide { width: 200px; height: 16px; background: #f59e0b; }
HTML
<div class="grid auto-min"><div><div class="wide"></div>A</div><div>B</div><div>C</div></div>
<div class="grid zero-min"><div><div class="wide"></div>A</div><div>B</div><div>C</div></div>
Resultlive, in your browser
What’s happening
  1. Both grids are 300px wide with no gaps, so "equal" would mean 100px each.
  2. In .auto-min the first column contains a 200px box. Its 1fr is really minmax(auto, 1fr), so its minimum is the content's min-content width, 200px. The browser can't give it 100px, so it freezes that column at 200px and splits the remaining 100px between the other two: measured [200, 50, 50].
  3. In .zero-min, minmax(0, 1fr) sets the minimum to 0, so the columns are truly equal: [100, 100, 100]. The wide child keeps its 200px and overflows its column instead.
  4. Which one you want depends on the content. For a code block or a table, overflowing (and giving that item overflow: auto to scroll) is usually right; for a sidebar with a long word, overflow-wrap: anywhere on the text fixes it at the source. Either way, reach for minmax(0, 1fr) when "equal columns" must stay equal.
Advancedauto tracks stretch when nothing else takes the space

auto is content-sized, but only as a minimum. When there are no fr tracks, any leftover space is shared between the auto tracks, because the container's justify-content defaults to normal, which behaves as stretch for grid tracks. Add one fr track and it takes all the leftover instead: that's the standard "label column sized to its content, field column takes the rest" pattern.

auto auto vs auto 1fr
CSS
.grid {
  display: grid;
  width: 400px;
  margin-bottom: 10px;
  background: #f1f5f9;
}
.two-auto { grid-template-columns: auto auto; }
.auto-fr { grid-template-columns: auto 1fr; }
.grid > div { height: 30px; background: #ccfbf1; color: #134e4a; }
.grid > div:last-child { background: #99f6e4; }
.box { width: 80px; height: 30px; }
HTML
<div class="grid two-auto"><div><div class="box">auto</div></div><div><div class="box">auto</div></div></div>
<div class="grid auto-fr"><div><div class="box">auto</div></div><div><div class="box">1fr</div></div></div>
Resultlive, in your browser
What’s happening
  1. Each track's content is an 80px box, and the containers are 400px wide.
  2. In .two-auto both tracks start at 80px, leaving 240px. With no flexible track to take it, the default justify-content: normal stretches the auto tracks equally: 80 + 120 = 200px each, measured.
  3. In .auto-fr the fr track is sized from the leftover after the auto track is at its content size, so auto stays at 80px and 1fr gets 320px.
  4. If you see auto columns mysteriously wider than their content, this stretching is why. Setting justify-content: start on the container also stops it: the test sets it on .two-auto and both tracks drop back to 80px.
Work out the fr size

A grid is 500px wide with grid-template-columns: 200px 1fr 1fr and gap: 10px. How wide is each 1fr column?

Show answer

140px each.

The quiz grid, measured
CSS
.grid { display: grid; width: 500px; grid-template-columns: 200px 1fr 1fr; gap: 10px; }
.grid > div { height: 30px; background: #fef3c7; color: #78350f; }
HTML
<div class="grid"><div>200px</div><div>1fr</div><div>1fr</div></div>
Resultlive, in your browser
What’s happening
  1. Take out everything that isn't flexible: the 200px track and two 10px gaps → 500 − 200 − 20 = 280px left.
  2. There are 2fr in total, so 1fr = 280 / 2 = 140px, which is what the browser measures.
  3. Percentages would get this wrong: 200px 1fr 1fr can't be written as percentages without doing the gap arithmetic by hand.

#repeat(), minmax() and auto-fill vs auto-fit

Added

Three functions make track lists short and adaptive. repeat(n, tracks) writes the same track (or pattern of tracks) n times: repeat(3, 1fr) is 1fr 1fr 1fr, and repeat(2, 60px 1fr) is 60px 1fr 60px 1fr. minmax(min, max) gives a track a range: never smaller than min, never bigger than max. Combined, repeat(auto-fill, minmax(150px, 1fr)) is the one-line responsive grid: "as many columns as fit, each at least 150px, then stretch them to fill the row", with no media queries.

The count comes from the minimum. With auto-fill or auto-fit instead of a number, the browser creates as many tracks as fit, using each track's minimum (here 150px) plus the gaps: count = floor((available + gap) / (min + gap)). Then 1fr as the maximum stretches those tracks to use up the remainder.

The difference between the two keywords only shows when there are fewer items than tracks. auto-fill keeps the empty tracks, so items stay at the column width and the row has empty slots at the end. auto-fit collapses empty tracks to 0, and the fr tracks with items in them share all the space. A mnemonic: auto-fill fills the row with tracks; auto-fit fits the tracks to the items.

Three items, auto-fill vs auto-fit
CSS
.grid {
  display: grid;
  gap: 10px;
  margin-bottom: 10px;
  outline: 2px dashed #94a3b8;
}
.fill { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.fit { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.grid > div { height: 40px; background: #dbeafe; color: #1e3a8a; padding: 0 6px; }
.fit > div { background: #dcfce7; color: #166534; }
HTML
<div class="grid fill"><div>fill 1</div><div>fill 2</div><div>fill 3</div></div>
<div class="grid fit"><div>fit 1</div><div>fit 2</div><div>fit 3</div></div>
Resultlive, in your browser
What’s happening
  1. Each grid is the frame width minus the body's 12px margins: 336px in a 360px frame, 776px in an 800px one. The test checks that first.
  2. The browser works out the track count from the minimum: floor((336 + 10) / (150 + 10)) = 2 at 360px, and floor((776 + 10) / 160) = 4 at 800px. Then 1fr stretches the tracks: two of (336 − 10) / 2 = 163px, or four of (776 − 30) / 4 = 186.5px.
  3. At 360px both grids are identical ("163px 163px"): three items need two rows, so no track is empty and the keywords don't differ.
  4. At 800px there are four tracks but three items. auto-fill keeps the fourth, empty track: items stay 186.5px and a gap is left at the end of the row. auto-fit collapses the empty track to 0px (the computed value literally reads "252px 252px 252px 0px") and the gaps next to it collapse too, so the three items share 776 − 20 = 756px → 252px each, and the last one ends at the grid's edge.
  5. The same happens at 700px: four tracks of 161.5px with auto-fill, three items of 218.7px with auto-fit. Resize your window and watch the live result reflow: the frame's width is the viewport these grids see.
  6. Pick auto-fill when items should keep a consistent size (a product grid where one result shouldn't become a giant card), and auto-fit when a few items should spread out to use the row.
AdvancedGuarding the minimum with min()

minmax(150px, 1fr) has a hidden assumption: the container is at least 150px wide. In a narrower container (a phone at 320px with padding, or a narrow sidebar), the one track still insists on 150px and overflows. Writing the minimum as min(150px, 100%) says "150px, unless the container is narrower, then the container's width".

A 120px-wide container
CSS
.grid {
  display: grid;
  width: 120px;
  gap: 10px;
  margin-bottom: 10px;
  outline: 2px dashed #94a3b8;
}
.plain { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.guarded { grid-template-columns: repeat(auto-fill, minmax(min(150px, 100%), 1fr)); }
.grid > div { height: 30px; background: #fee2e2; color: #7f1d1d; }
.guarded > div { background: #dcfce7; color: #166534; }
HTML
<div class="grid plain"><div>150px min</div></div>
<div class="grid guarded"><div>guarded</div></div>
Resultlive, in your browser
What’s happening
  1. Both containers are only 120px wide, narrower than the 150px minimum.
  2. .plain still creates one track (there's always at least one), and minmax(150px, 1fr) can't go below 150px: measured 150px, so it sticks 30px out of the dashed outline and would cause horizontal scrolling on a phone.
  3. In .guarded the minimum is min(150px, 100%), and 100% here is the container's 120px, so the minimum becomes 120px and the track fits exactly.
  4. In wider containers min() picks 150px and the grid behaves exactly like the plain version. That's why repeat(auto-fill, minmax(min(150px, 100%), 1fr)) is the robust form of the pattern (see Math functions: calc(), min(), max() and clamp()).
AdvancedRules of repeat()

A few rules come up in interviews and in DevTools when a declaration is silently dropped:

What repeat() accepts
CSS
.grid {
  display: grid;
  width: 400px;
  grid-template-columns: repeat(2, 60px 1fr);
}
.grid > div { height: 30px; background: #ede9fe; color: #4c1d95; }
.grid > div:nth-child(odd) { background: #ddd6fe; }
HTML
<div class="grid"><div>60px</div><div>1fr</div><div>60px</div><div>1fr</div></div>
Resultlive, in your browser
What’s happening
  1. repeat(2, 60px 1fr) repeats the whole pattern, so the grid has four tracks: 60px, then the two 1fr tracks share 400 − 120 = 280px → measured [60, 140, 60, 140].
  2. repeat(auto-fill, 100px) is valid, but repeat(auto-fill, 1fr) is not (CSS.supports says false): to count how many tracks fit, the browser needs a definite size, and 1fr alone doesn't have one. minmax(100px, 1fr) provides the definite minimum, so it's valid.
  3. A track list may contain only one auto-fill/auto-fit repeat; two of them are invalid. Fixed tracks before and after it are fine: 200px repeat(auto-fill, 100px) 50px is valid.
  4. minmax(1fr, 200px) is invalid because fr is allowed only as the maximum of minmax(). Invalid declarations are dropped at parse time, so the grid falls back to whatever the cascade had before, often no columns at all, which is why a typo here looks like "grid stopped working".
How many columns at 700px?

A page has a <div> with display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px, inside a body with 12px margins. The viewport is 700px wide and there are three items. How many columns are there, and how wide is each item? What changes with auto-fit?

Show answer

Four columns of 161.5px, with the fourth one empty. With auto-fit the empty track collapses and the three items are 218.7px each.

What’s happening
  1. Available width: 700 − 24 = 676px.
  2. Count from the minimum: floor((676 + 10) / (150 + 10)) = floor(4.29) = 4 tracks.
  3. Stretch: four tracks and three gaps → (676 − 30) / 4 = 161.5px each. The main example's test measures exactly this at 700px ("161.5px 161.5px 161.5px 161.5px").
  4. With auto-fit, the fourth track has no item, so it collapses to 0 along with its gap: (676 − 20) / 3 = 218.67px, measured as 218.7px.

#Placing items on lines and spans

Added

By default every item is auto-placed into the next free cell. To place one yourself, you give it lines: grid-column-start / grid-column-end and grid-row-start / grid-row-end, usually through the shorthands grid-column: <start> / <end> and grid-row: <start> / <end>. A value can be a line number (2, -1), a span (span 2: "cover two tracks from wherever I start"), or a named line.

grid-area is the shorthand for all four, in the order row-start / column-start / row-end / column-end: the two starts, then the two ends, going anticlockwise from the top (the opposite of margin's clockwise order, which trips everybody up). Leave out the end and the item spans one track. Items can overlap: two items placed in the same cell stack, later ones on top, and z-index works on grid items without position.

Line numbers, spans and grid-area
CSS
.grid {
  display: grid;
  grid-template-columns: repeat(4, 60px);
  grid-template-rows: repeat(3, 40px);
  gap: 10px;
}
.grid > div { background: #dbeafe; color: #1e3a8a; padding: 2px 6px; font-size: 13px; }
.a { grid-column: 1 / 3; }
.b { grid-column: span 2; background: #fde68a !important; color: #78350f !important; }
.c { grid-area: 2 / 1 / 4 / 2; background: #fbcfe8 !important; color: #831843 !important; }
.d { grid-column: 2 / -1; grid-row: 3; background: #bbf7d0 !important; color: #14532d !important; }
HTML
<div class="grid">
  <div class="a">a 1/3</div>
  <div class="b">b span 2</div>
  <div class="c">c</div>
  <div class="d">d 2/-1, row 3</div>
  <div class="e">e</div>
</div>
Resultlive, in your browser
What’s happening
  1. The grid has four 60px columns and three 40px rows with 10px gaps, so column lines sit at 0, 70, 140, 210, 270 and row lines at 0, 50, 100, 140.
  2. Items with a definite row are placed first: c (grid-area: 2 / 1 / 4 / 2 = row 2 to 4, column 1 to 2) lands at 0, 50 and is 90px tall (two rows plus a gap). d (grid-column: 2 / -1, grid-row: 3) covers column 2 to the last line in row 3: measured 200px wide at 70, 100.
  3. Then the rest in source order. a has columns 1 / 3 and no row, so it takes row 1: 130px wide at 0, 0. b has only span 2, so it goes after a in row 1, columns 3 to 4: 140, 0.
  4. e has no placement at all. Row 1 is full, row 2 column 1 belongs to c, so it lands in row 2 column 2: 70, 50.
  5. The longhands confirm the grid-area order: grid-row-start: 2, grid-column-start: 1, grid-row-end: 4, grid-column-end: 2. Starts first, then ends, rows before columns.
AdvancedOverlap and z-index

Placing two items in the same area is allowed and is how grid does overlays (a caption over an image, a badge over a card) without position: absolute. They paint in order-modified document order, so the later one wins, and z-index changes that even though the items are position: static.

Two items in one cell
CSS
.grid {
  display: grid;
  grid-template-columns: 200px;
  grid-template-rows: 120px;
}
.photo, .caption { grid-area: 1 / 1; }
.photo { background: linear-gradient(135deg, #93c5fd, #c4b5fd); }
.caption { align-self: end; background: rgb(15 23 42 / 0.75); color: #fff; padding: 6px 8px; }
.raised .photo { z-index: 1; }
.raised { margin-top: 10px; }
HTML
<div class="grid normal"><div class="photo"></div><div class="caption">Caption on top</div></div>
<div class="grid raised"><div class="photo"></div><div class="caption">Hidden behind</div></div>
Resultlive, in your browser
What’s happening
  1. grid-area: 1 / 1 puts both .photo and .caption in row 1, column 1. Grid doesn't push overlapping items apart; they share the cell.
  2. .caption has align-self: end, so it sits at the bottom of the cell instead of stretching over the whole photo.
  3. In the first grid nothing else is set, so source order decides: the caption comes later and document.elementFromPoint over it returns the caption.
  4. In the second grid .photo gets z-index: 1 while staying position: static. Grid items (like flex items) honour z-index anyway, so the photo now paints over the caption and elementFromPoint returns .photo. See z-index and stacking contexts for what that does to stacking.
AdvancedNamed lines: the full-bleed layout

Lines can be named in the track list with brackets: [content-start] 300px [content-end]. A pair named x-start and x-end also lets you write just grid-column: x, which means "from x-start to x-end". The most useful layout built on this is the full-bleed page: text stays in a readable centre column, while chosen elements (images, banners) run edge to edge, all from one grid with no wrapper divs.

One centre column, one full-width element
CSS
.page {
  display: grid;
  grid-template-columns:
    [full-start] 1fr
    [content-start] minmax(0, 300px) [content-end]
    1fr [full-end];
  row-gap: 8px;
}
.page > * { grid-column: content; height: 30px; background: #e0e7ff; color: #312e81; margin: 0; }
.page > .bleed { grid-column: full; background: linear-gradient(90deg, #f0abfc, #93c5fd); }
HTML
<div class="page">
  <p>Paragraph</p>
  <div class="bleed">Full-bleed banner</div>
  <p>Paragraph</p>
</div>
Resultlive, in your browser
What’s happening
  1. The track list has three columns, and the lines between them carry names: full-start, content-start, content-end, full-end. The middle track is at most 300px, and the two 1fr tracks share whatever is left equally.
  2. grid-column: content is short for content-start / content-end, so every child sits in the centre track: measured 300px wide and centred ((336 − 300) / 2 = 18px in from each side at 360px, (776 − 300) / 2 = 238px at 800px).
  3. .bleed says grid-column: full, which resolves to full-start / full-end: the full grid width, 336px or 776px.
  4. The minmax(0, 300px) minimum of 0 matters: on a screen narrower than 300px the centre track can shrink instead of forcing horizontal scrolling. Blogs and documentation sites use this pattern so content authors can drop a wide image into the flow without breaking out of a container.
AdvancedVisual order is not reading order

Placement, order, grid-auto-flow: dense and template areas move items visually without changing the DOM. Screen readers and the Tab key follow the DOM, so a layout that looks left-to-right can be read and focused in a different order. That's a WCAG failure (1.3.2 Meaningful Sequence, 2.4.3 Focus Order) when the order carries meaning.

DOM order 1, 2, 3 shown as 3, 2, 1
CSS
.grid {
  display: grid;
  grid-template-columns: repeat(3, 80px);
  gap: 10px;
}
.grid a { display: block; padding: 8px; background: #fef3c7; color: #78350f; }
.one { grid-column: 3; grid-row: 1; }
.two { grid-column: 2; grid-row: 1; }
.three { grid-column: 1; grid-row: 1; }
HTML
<nav class="grid">
  <a class="one" href="#first">First</a>
  <a class="two" href="#second">Second</a>
  <a class="three" href="#third">Third</a>
</nav>
Resultlive, in your browser
What’s happening
  1. The DOM order of the links is First, Second, Third. A keyboard user pressing Tab visits them in that order.
  2. .one is placed in column 3, .two in column 2 and .three in column 1, all in row 1, so visually the row reads Third, Second, First: the test measures their x positions decreasing in DOM order.
  3. A sighted keyboard user sees focus jump from the right edge leftwards, which is confusing; a screen reader announces "First" while a sighted helper is pointing at the leftmost link, "Third".
  4. The fix is to keep the DOM in the order you want it read, and use placement for layout that doesn't change meaning. The newer reading-flow: grid-order property (Chrome and Edge 137+, not in Firefox or Safari as of October 2026, so not Baseline) tells the browser to follow the visual grid order for focus and reading. Chrome 155 accepts it (CSS.supports is true); this test can't press Tab, so the focus order itself isn't checked here.
Backwards lines

In a four-column grid, an item has grid-column: 4 / 2. Where does it go?

Show answer

It covers columns 2 and 3: from line 2 to line 4.

Start after end
CSS
.grid { display: grid; grid-template-columns: repeat(4, 50px); }
.grid > div { height: 30px; background: #e2e8f0; }
.grid > .x { grid-column: 4 / 2; background: #fca5a5; }
HTML
<div class="grid"><div class="x">4 / 2</div></div>
Resultlive, in your browser
What’s happening
  1. When the start line comes after the end line, the grid placement algorithm swaps them, so 4 / 2 is treated as 2 / 4.
  2. The item starts at line 2 (50px in) and is two tracks wide (100px), measured.
  3. The computed grid-column-start still says 4: the swap is part of placement, not a rewrite of your value. If start and end are the same line, the end is dropped instead and the item spans one track.

#Named template areas

Added

grid-template-areas lets you draw the layout as ASCII art. Each string is one row, each word in it is one cell, and repeating a name across neighbouring cells makes that named area bigger. Items then join an area by name with grid-area: header. A . (or a run of dots like ...) marks an empty cell.

It's the most readable way to describe a page layout, and it makes responsive changes cheap: the items keep grid-area: main forever, and a media query only redraws the map. The rules are strict, though: every row must have the same number of cells, and every named area must be a rectangle. Break either rule and the whole declaration is invalid and ignored.

One column on phones, three columns from 600px
CSS
.layout {
  display: grid;
  gap: 8px;
  grid-template-areas:
    "header"
    "nav"
    "main"
    "aside"
    "footer";
}
@media (min-width: 600px) {
  .layout {
    grid-template-columns: 120px 1fr 100px;
    grid-template-areas:
      "header header header"
      "nav    main   aside"
      "footer footer footer";
  }
}
.layout > * { height: 40px; padding: 0 8px; background: #e0e7ff; color: #312e81; }
header { grid-area: header; }
nav { grid-area: nav; background: #dcfce7 !important; color: #14532d !important; }
main { grid-area: main; background: #fef3c7 !important; color: #78350f !important; }
aside { grid-area: aside; background: #fce7f3 !important; color: #831843 !important; }
footer { grid-area: footer; }
HTML
<div class="layout">
  <header>header</header>
  <main>main</main>
  <nav>nav</nav>
  <aside>aside</aside>
  <footer>footer</footer>
</div>
Resultlive, in your browser
What’s happening
  1. Mobile first: the base rule draws a single column with five rows, one name per string. With no grid-template-columns, there is one auto column, which stretches to the full 336px at a 360px viewport.
  2. The items are in the DOM as header, main, nav, aside, footer, but the map puts nav above main, so the measured tops are 0, 48, 96, 144, 192 in map order. Areas place items wherever the map says, regardless of source order (keep the reading-order caveat from Placing items on lines and spans in mind).
  3. From 600px the media query redraws the map with three columns. header is written three times in the first row, so it's one area spanning all three columns: 776px wide at 800px.
  4. Row 2 is nav main aside. The columns are 120px, 1fr and 100px, and the fr column gets 776 − 120 − 100 − 16 = 540px. Measured: nav at 0 (120px), main at 128 (540px), aside at 676 (100px).
  5. Nothing on the items changed between the two layouts: each still has its grid-area name (the computed grid-area of main is "main"). That's the strength of template areas: the layout lives in one place, on the container.
AdvancedEmpty cells, invalid maps and the grid-template shorthand
Dots, an L-shaped area, and grid-template
CSS
.dots {
  display: grid;
  grid-template-columns: repeat(3, 60px);
  grid-template-areas: "a ... b";
  margin-bottom: 10px;
}
.dots > div { height: 30px; background: #dbeafe; }
.dots .a { grid-area: a; }
.dots .b { grid-area: b; }

.broken {
  display: grid;
  grid-template-areas: "x x" "y y";
  grid-template-areas: "a a" "a b" "a a";
}

.short {
  display: grid;
  grid-template:
    "head head" 40px
    "side body" 60px
    / 100px 1fr;
  width: 300px;
}
.short > div { background: #fef3c7; }
.short .head { grid-area: head; background: #fde68a; }
.short .side { grid-area: side; }
.short .body { grid-area: body; background: #fffbeb; }
.short .tag { grid-column: body-start; grid-row: body-start; justify-self: end; align-self: start; background: #fb7185; color: #fff; padding: 0 4px; }
HTML
<div class="dots"><div class="a">a</div><div class="b">b</div></div>
<div class="broken"></div>
<div class="short"><div class="head">head</div><div class="side">side</div><div class="body">body</div><div class="tag">tag</div></div>
Resultlive, in your browser
What’s happening
  1. In .dots, "a ... b" is three cells: a, one empty cell (a run of dots with no spaces counts as a single .), and b. So b is in column 3, measured 120px in.
  2. .broken has two declarations. The second one draws a in an L shape (column 1 in all three rows, plus column 2 in rows 1 and 3), which isn't a rectangle. The browser rejects it at parse time, exactly like a typo, so the first declaration ("x x" "y y") is what's computed. Without that earlier declaration it would be none, and items that name areas would pile up outside the grid, as the quiz below shows.
  3. grid-template combines all three: each area string can be followed by its row height, and the column sizes come after a /. Here it produces rows 40px 60px and columns 100px 200px (the 1fr gets what's left of 300px), and body lands at column 2, row 2: 100, 40.
  4. Every named area also creates implicit named lines: area body gives you body-start and body-end in both directions. .tag uses grid-column: body-start; grid-row: body-start and lands in the same cell as body (measured: same top, same right edge, thanks to justify-self: end), overlapping it like a badge.
Is this map valid?
CSS
grid-template-areas:
  "nav header header"
  "nav main"
  "nav footer footer";

What happens?

Show answer

The declaration is invalid and ignored, because row 2 has two cells while rows 1 and 3 have three.

The quiz map, measured
CSS
.g {
  display: grid;
  grid-template-areas:
    "nav header header"
    "nav main"
    "nav footer footer";
}
.g2 {
  display: grid;
  grid-template-areas:
    "nav header header"
    "nav main   main"
    "nav footer footer";
}
.g, .g2 { margin-bottom: 10px; outline: 2px dashed #94a3b8; }
.g > *, .g2 > * { padding: 2px 6px; background: #e0e7ff; }
nav { grid-area: nav; background: #dcfce7 !important; }
header { grid-area: header; }
main { grid-area: main; background: #fef3c7 !important; }
footer { grid-area: footer; }
HTML
<div class="g"><nav>nav</nav><header>header</header><main>main</main><footer>footer</footer></div>
<div class="g2"><nav>nav</nav><header>header</header><main>main</main><footer>footer</footer></div>
Resultlive, in your browser
What’s happening
  1. Every string must have the same number of cells. Row 2 has nav main (two), rows 1 and 3 have three.
  2. So the whole declaration fails to parse and is dropped: the computed value is none. The items still say grid-area: nav, header and so on, but no lines with those names exist. The spec's rule for a missing line name is to treat every implicit line as having it, so each item is placed one track past the explicit grid: all four land in the same cell, implicit column 2, row 2, on top of each other (measured: same position; the first column and row are empty tracks). On screen you see only the last item, footer, in the dashed box.
  3. The fix is "nav main main". Then nav is a valid 1 × 3 rectangle down the left and main a 2 × 1 rectangle in row 2.
  4. DevTools' Styles pane shows a rejected declaration struck through, which is the fastest way to catch this.

#The implicit grid and auto-placement

Added

The tracks you declare are the explicit grid. Whenever an item lands outside it (because there are more items than cells, or because you placed one on line 7 of a 3-column grid), the browser adds tracks to hold it. Those are the implicit grid, and their size comes from grid-auto-rows and grid-auto-columns (default auto, so content-sized).

Items you don't place are positioned by the auto-placement algorithm. With the default grid-auto-flow: row it fills row by row, left to right, like text, creating new rows as needed. The common pattern is to declare only columns and let rows be implicit: grid-template-columns: repeat(3, 1fr); grid-auto-rows: minmax(100px, auto) means "three columns, as many rows as the content needs, each at least 100px and taller if an item needs it".

Three declared columns, rows created as needed
CSS
.grid {
  display: grid;
  grid-template-columns: repeat(3, 80px);
  grid-auto-rows: minmax(40px, auto);
  gap: 10px;
}
.grid > div { background: #ccfbf1; color: #134e4a; padding: 0 6px; }
.tall { height: 70px; background: #5eead4 !important; }
HTML
<div class="grid">
  <div>1</div><div>2</div><div>3</div>
  <div>4</div><div><div class="tall">5 (70px)</div></div><div>6</div>
  <div>7</div>
</div>
Resultlive, in your browser
What’s happening
  1. Only columns are declared, so the explicit grid has three column tracks and no rows at all.
  2. Auto-placement puts items 1 to 3 in a row, runs out of columns, and creates an implicit row 2 for items 4 to 6, then row 3 for item 7.
  3. Each implicit row is sized by grid-auto-rows: minmax(40px, auto): at least 40px, more if the content needs it. Row 2 holds item 5, whose content is 70px tall, so that row grows to 70px; the others stay at 40px. getComputedStyle reports the resolved rows, implicit ones included: "40px 70px 40px".
  4. Item 7 lands in row 3, column 1, measured 130px down (40 + 10 + 70 + 10), and the grid is 170px tall. Items 4 and 6 are stretched to 70px too (measured; the default align-items: stretch), which is why grid rows of cards line up without any height setting.
AdvancedPlacing outside the grid creates implicit tracks
An item on column 5 of a 3-column grid
CSS
.grid {
  display: grid;
  grid-template-columns: repeat(3, 60px);
  grid-auto-columns: 40px;
  gap: 10px;
}
.grid > div { height: 30px; background: #e0e7ff; }
.far { grid-column: 5; background: #fca5a5 !important; }
HTML
<div class="grid"><div>1</div><div>2</div><div class="far">5</div></div>
Resultlive, in your browser
What’s happening
  1. The explicit grid has three 60px columns, so lines 1 to 4.
  2. .far asks for column 5, which doesn't exist. The browser adds implicit columns 4 and 5, sized by grid-auto-columns: 40px; the computed track list is "60px 60px 60px 40px 40px".
  3. Column 4 stays empty but still takes its 40px and gaps, so .far starts at 180 + 40 + 40 = 260px, measured.
  4. .far only has a column, so auto-placement finds a row for it: row 1, column 5 is free, so it stays in row 1 beside items 1 and 2. Placing items far outside the grid by mistake (a wrong line number) is a common source of mysterious extra columns; DevTools' grid overlay shows every track, explicit and implicit.
Advancedgrid-auto-flow: dense fills the holes

When an item doesn't fit in the remaining space of a row (a span 3 item in a row with two free cells), auto-placement moves on and leaves a hole; it never goes back. grid-auto-flow: dense makes it go back and fill earlier holes with later items that fit. It's great for image galleries, and bad wherever order matters, because the visual order no longer follows the DOM.

The same four items, sparse and dense
CSS
.grid {
  display: grid;
  grid-template-columns: repeat(3, 60px);
  grid-auto-rows: 30px;
  gap: 6px;
  margin-bottom: 10px;
}
.dense { grid-auto-flow: dense; }
.grid > div { background: #fef3c7; color: #78350f; padding: 0 6px; }
.grid > .big { grid-column: span 3; background: #fde68a; }
HTML
<div class="grid sparse"><div>A</div><div class="big">B span 3</div><div>C</div><div>D</div></div>
<div class="grid dense"><div>A</div><div class="big">B span 3</div><div>C</div><div>D</div></div>
Resultlive, in your browser
What’s happening
  1. A takes row 1, column 1. B spans all three columns, which don't fit in what's left of row 1, so it goes to row 2.
  2. Sparse (the default): the placement cursor is now after B, and it only moves forward. C and D go to row 3, leaving two empty cells in row 1. Measured [column, row]: C at [0, 2], D at [1, 2], three rows in total.
  3. Dense: for every item the search restarts at the beginning of the grid, so C and D fill the holes in row 1 at columns 2 and 3. Two rows.
  4. Visually the dense grid reads A, C, D, then B, while the DOM (and so the Tab order and screen readers) says A, B, C, D. Use dense for content where order doesn't matter, such as a photo wall.
Advancedgrid-auto-flow: column
Filling down columns
CSS
.grid {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(2, 30px);
  grid-auto-columns: 60px;
  gap: 6px;
}
.grid > div { background: #ede9fe; color: #4c1d95; padding: 0 6px; }
HTML
<div class="grid"><div>1</div><div>2</div><div>3</div><div>4</div><div>5</div></div>
Resultlive, in your browser
What’s happening
  1. grid-auto-flow: column makes auto-placement fill down each column before moving right, and creates implicit columns instead of rows.
  2. Two rows are declared, so items go 1 and 2 in column 1, 3 and 4 in column 2, 5 in column 3. The measured positions confirm it.
  3. The new columns are implicit and sized by grid-auto-columns: 60px, giving "60px 60px 60px". This is handy for a horizontally scrolling row of cards, where you want "one more column per item" rather than wrapping.
AdvancedGotcha: 1 / -1 doesn't reach implicit rows
A sidebar that should span every row
CSS
.grid {
  display: grid;
  grid-template-columns: 60px 120px;
  grid-auto-rows: 30px;
  gap: 6px;
  margin-bottom: 10px;
}
.grid > div { grid-column: 2; background: #e0f2fe; }
.grid > .side { grid-column: 1; grid-row: 1 / -1; background: #fca5a5; }
.fixed { grid-template-rows: repeat(3, 30px); }
HTML
<div class="grid implicit"><div class="side">side</div><div>a</div><div>b</div><div>c</div></div>
<div class="grid fixed"><div class="side">side</div><div>a</div><div>b</div><div>c</div></div>
Resultlive, in your browser
What’s happening
  1. In .implicit no rows are declared, so the explicit grid has exactly one row line, line 1, and -1 (the last explicit line) is also line 1.
  2. grid-row: 1 / -1 therefore means 1 / 1. When start and end are the same line, the end is dropped and the item spans one track: the sidebar is measured at 30px, one row, even though a, b and c (all in column 2) create three implicit rows.
  3. In .fixed the three rows are explicit (repeat(3, 30px)), so -1 is line 4 and the sidebar spans all of them: 30 × 3 + 2 × 6 = 102px.
  4. Fixes: declare the rows explicitly, use grid-row: span 3 when you know the count, or restructure so the main content is one item in one row next to the sidebar.
How tall is the grid?

A grid has grid-template-columns: repeat(3, 1fr); grid-auto-rows: 50px; gap: 10px and ten empty divs. How tall is it?

Show answer

230px: four rows of 50px and three gaps.

The quiz grid, measured
CSS
.grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 50px; gap: 10px; }
.grid > div { background: #dbeafe; }
HTML
<div class="grid"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
Resultlive, in your browser
What’s happening
  1. Ten items in three columns need ceil(10 / 3) = 4 rows; the last row holds just item 10.
  2. None of the rows are declared, so all four are implicit and sized by grid-auto-rows: 50px.
  3. Height: 4 × 50 + 3 × 10 = 230px, measured. Empty items still occupy cells; it's the number of items, not their content, that decides the row count.

#Aligning in grid

Added

Grid alignment has two axes and three levels, and the property names encode both. justify-* works along the inline axis (the row direction, left to right in English) and align-* along the block axis (top to bottom). Then:

  • *-content moves the tracks inside the container. It only does something when the tracks add up to less than the container (fixed-size tracks in a bigger container).
  • *-items on the container sets the default alignment of every item inside its own grid area.
  • *-self on one item overrides *-items for that item.

The default is stretch: an item without a fixed size fills its area in both directions, which is why grid cells look like table cells. The place-* shorthands set both axes, block axis first: place-items: center is align-items: center; justify-items: center, and place-items: end start is align-items: end; justify-items: start.

PropertySet onMovesAxis
justify-content / align-contentcontainerthe whole set of tracksinline / block
justify-items / align-itemscontainerevery item within its areainline / block
justify-self / align-selfitemone item within its areainline / block
place-content, place-items, place-selfas aboveboth axes (block value first)both
Tracks spread out, items centred in their cells
CSS
.grid {
  display: grid;
  width: 300px;
  height: 200px;
  grid-template-columns: 100px 100px;
  grid-template-rows: 60px 60px;
  justify-content: space-between;
  align-content: center;
  place-items: center;
  background: repeating-linear-gradient(45deg, #f1f5f9 0 6px, #e2e8f0 6px 12px);
}
.grid > div { width: 40px; height: 20px; background: #2563eb; }
HTML
<div class="grid"><div></div><div></div><div></div><div></div></div>
Resultlive, in your browser
What’s happening
  1. The container is 300 × 200, and the tracks only add up to 200 × 120, so there's 100px of free space across and 80px down. That free space is what *-content distributes.
  2. justify-content: space-between pushes the two column tracks to the edges: column 1 at 0, column 2 at 300 − 100 = 200px.
  3. align-content: center splits the vertical free space equally above and below: rows start at 40px and 100px.
  4. Inside each 100 × 60 cell, place-items: center (both align-items and justify-items become center, as the test reads back; and place-items: end start parses to align-items: end, justify-items: start) centres each 40 × 20 item: 30px in from the cell's left and 20px down. So the items are measured at x = 30 and 230, y = 60 and 120.
  5. Two levels, two jobs: *-content positioned the tracks in the container; *-items positioned the items in their tracks. Without fixed track sizes (1fr columns, say) the tracks would fill the container and justify-content would have nothing to distribute.
AdvancedWhat stretches by default, and what doesn't

stretch only stretches what has no size of its own. An item with a set height keeps it (and is aligned to the start). Images and other replaced elements with an aspect ratio are a special case: their default (normal) behaves as start, not stretch, so an <img> in a tall grid row keeps its proportions instead of being squashed.

A 100px-tall row with three different items
CSS
.grid {
  display: grid;
  grid-template-columns: repeat(5, 80px);
  grid-template-rows: 100px;
  gap: 10px;
}
.grid > * { background: #dcfce7; }
.sized { height: 40px; }
.m { width: 40px; height: 30px; margin: auto; background: #a78bfa !important; }
.self { align-self: end; justify-self: center; width: 40px; height: 30px; background: #fbbf24 !important; }
HTML
<div class="grid">
  <div class="plain"></div>
  <div class="sized"></div>
  <img alt="" width="40" height="40" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Ccircle cx='20' cy='20' r='18' fill='%232563eb'/%3E%3C/svg%3E">
  <div class="self"></div>
  <div class="m"></div>
</div>
Resultlive, in your browser
What’s happening
  1. The single row is 100px tall, and the five columns are 80px wide.
  2. .plain has no size, so the default stretch makes it 100px tall and 80px wide, filling its cell.
  3. .sized has height: 40px. Stretch never overrides a definite size, so it stays 40px and sits at the top of its cell (y = 0).
  4. The <img> (an inline SVG data URI, 40 × 40) also stays 40 × 40 at the top-left of its cell (x = 180), even though it has no CSS height: for an element with an aspect ratio, normal alignment behaves as start in both axes. If you want an image to fill its cell, set width: 100%; height: 100%; object-fit: cover (see aspect-ratio and object-fit).
  5. .self overrides the container for itself: align-self: end puts it at the bottom (100 − 30 = 70px) and justify-self: center centres it across: 270 + (80 − 40) / 2 = 290px. margin: auto on the last item centres it in both axes (360 + 20, 35), because auto margins absorb the free space in its area.
place-items vs place-content

Two 300px-wide grid containers each hold two items: one contains a 60px-wide box, the other a 120px-wide box. Neither container declares any columns. The first has place-items: center, the second place-content: center. Do the items end up the same width?

Show answer

No. With place-items: center each item is its own content width (60px and 120px), each centred. With place-content: center both items are 120px wide, because they stretch to fill a track sized by the widest one.

The quiz, measured
CSS
.grid { display: grid; width: 300px; height: 80px; margin-bottom: 10px; outline: 2px dashed #94a3b8; }
.items { place-items: center; }
.content { place-content: center; }
.grid > div { background: #bfdbfe; }
.w60 { width: 60px; height: 20px; }
.w120 { width: 120px; height: 20px; }
HTML
<div class="grid items"><div><div class="w60"></div></div><div><div class="w120"></div></div></div>
<div class="grid content"><div><div class="w60"></div></div><div><div class="w120"></div></div></div>
Resultlive, in your browser
What’s happening
  1. With no columns declared, each grid has one implicit auto column, and the two items stack in two implicit rows.
  2. place-items: center sets justify-items: center, so the items stop stretching and shrink to their content: 60px and 120px, each centred in a column that is still the full 300px (measured track: "300px"). They sit at 120px and 90px.
  3. place-content: center sets justify-content: center, which centres the track instead. An auto track that isn't stretched is as wide as its widest content, 120px, so it sits at 90px. The items keep the default justify-items: stretch and fill that track: both 120px wide.
  4. For one fixed-size child the two look identical, which is why both appear in "how do I centre a div" answers (see Centering things: every way). With several children, place-items centres each one, place-content centres the group.

#Subgrid

Added

A nested grid normally knows nothing about its parent: a card that is display: grid gets its own tracks, sized by its own content. That's a problem for a row of cards where the titles, bodies and buttons should line up across cards even when one title wraps to two lines. Subgrid lets a grid item borrow its parent's tracks instead of defining its own: grid-template-rows: subgrid (or grid-template-columns: subgrid, or both) on an item that spans some parent tracks makes its children sit on the parent's lines.

The mental model: the card stops being a separate grid and becomes a window onto the parent grid. The parent sizes each shared row from the content of every card in it, so the tallest title decides the title row for all of them. Subgrid is Baseline 2023 (in all major engines since Chrome and Edge 117 in September 2023) and widely available since March 2026, as listed on MDN and the web-features data in October 2026.

Card titles of different lengths, with and without subgrid
CSS
.cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin-bottom: 12px;
}
.card { grid-row: span 3; }
.with-subgrid .card { display: grid; grid-template-rows: subgrid; }
.without .card { display: flex; flex-direction: column; gap: 10px; }
.title { background: #c7d2fe; color: #1e1b4b; }
.body { background: #e0e7ff; color: #312e81; }
.foot { height: 30px; background: #a5b4fc; color: #1e1b4b; }
HTML
<div class="cards with-subgrid">
  <div class="card"><div class="title" style="height:30px">Short</div><div class="body" style="height:40px">Body</div><div class="foot">Buy</div></div>
  <div class="card"><div class="title" style="height:60px">A much longer title that wraps</div><div class="body" style="height:80px">Body</div><div class="foot">Buy</div></div>
  <div class="card"><div class="title" style="height:30px">Short</div><div class="body" style="height:40px">Body</div><div class="foot">Buy</div></div>
</div>
<div class="cards without">
  <div class="card"><div class="title" style="height:30px">Short</div><div class="body" style="height:40px">Body</div><div class="foot">Buy</div></div>
  <div class="card"><div class="title" style="height:60px">A much longer title that wraps</div><div class="body" style="height:80px">Body</div><div class="foot">Buy</div></div>
  <div class="card"><div class="title" style="height:30px">Short</div><div class="body" style="height:40px">Body</div><div class="foot">Buy</div></div>
</div>
Resultlive, in your browser
What’s happening
  1. .cards is a three-column grid with gap: 10px. Each .card spans three rows of it (grid-row: span 3), so the parent gets three implicit rows: one for titles, one for bodies, one for footers.
  2. In the first set, each card is display: grid; grid-template-rows: subgrid. The card doesn't create rows; its three children land on the parent's three rows. The parent sizes each row from the content of all the cards in it: the title row is 60px (the long title), the body row 80px, the footer row 30px, which getComputedStyle reports as "60px 80px 30px".
  3. So every body starts at 70px (60 + 10 gap) and every footer at 160px, measured identical in all three cards, even though only the middle card has the long title.
  4. A subgrid uses the parent's gaps unless it sets its own: the card's own row-gap is still normal, and the measured space between title row and body row is the parent's 10px. Setting row-gap: 0 on the card would override it.
  5. In the second set, each card is an independent flex column. Its body follows its own title, so the bodies start at 40, 70, 40 and the footers at 90, 160, 90: the ragged look subgrid was made to fix.
AdvancedSubgrid columns: aligning labels and fields

The same idea works across columns. A form where each row is its own element (a <div> or <label> wrapper) normally sizes its label column per row. With grid-template-columns: subgrid, every row shares the parent's auto column, so all fields start after the widest label.

Labels of different widths, one shared column
CSS
.form {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 8px 12px;
  width: 360px;
}
.field { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; }
.label { height: 24px; background: #fef3c7; }
.input { height: 24px; background: #e0f2fe; border: 1px solid #7dd3fc; }
HTML
<div class="form">
  <div class="field"><div class="label" style="width:60px">Name</div><div class="input"></div></div>
  <div class="field"><div class="label" style="width:110px">Email address</div><div class="input"></div></div>
  <div class="field"><div class="label" style="width:40px">Age</div><div class="input"></div></div>
</div>
Resultlive, in your browser
What’s happening
  1. .form has two columns: auto for labels and 1fr for inputs, with a 12px column gap.
  2. Each .field spans both columns (1 / -1) and uses grid-template-columns: subgrid, so its label and input sit in the parent's two columns instead of the field's own.
  3. The parent sizes the auto column from every label in it, and the widest is 110px ("Email address"). The computed parent columns are "110px 238px" (360 − 110 − 12 = 238).
  4. So every input starts at 122px, measured identical in all three rows. Without subgrid, each field would be its own grid and its input would start right after its own label.
AdvancedSubgrid rules worth knowing
  • Line numbers restart. Inside a subgrid, line 1 is the subgrid's own first line, not the parent's. Named lines from the parent are inherited, and you can add names of your own after the keyword: grid-template-columns: subgrid [a] [b].
  • No implicit tracks in a subgridded direction. A subgrid has exactly as many tracks as it spans. An item placed beyond them is clamped into the last track instead of creating a new one (demo below).
  • The other direction is normal. grid-template-rows: subgrid alone leaves the subgrid's columns as its own, defined however you like.
  • Gaps are inherited, padding still counts. The subgrid uses the parent's gaps unless it sets its own. Its own padding and border insets the children in its first and last tracks (they act like extra margin on those items), while the track sizes still come from the parent.
  • Fallback. Browsers without subgrid ignore the subgrid value, so the item gets an ordinary grid. Write the plain grid first, then the subgrid inside @supports (grid-template-rows: subgrid) if older browsers matter (see Feature detection with @supports).
A subgrid has no implicit columns
CSS
.parent {
  display: grid;
  grid-template-columns: repeat(4, 50px);
  gap: 10px;
}
.sub {
  grid-column: 1 / 3;
  display: grid;
  grid-template-columns: subgrid;
  background: #f1f5f9;
}
.sub > div { height: 30px; background: #c4b5fd; }
.sub > .far { grid-column: 4; background: #f87171; }
.sub.padded { padding-left: 10px; }
.sub.padded > .far { grid-column: auto; }
HTML
<div class="parent"><div class="sub"><div class="near">1</div><div class="far">4?</div></div></div>
<div class="parent" style="margin-top:10px"><div class="sub padded"><div class="near">1</div><div class="far">2</div></div></div>
Resultlive, in your browser
What’s happening
  1. .sub spans parent columns 1 and 2 (110px, two tracks plus a gap) and borrows them with grid-template-columns: subgrid. It has exactly two columns.
  2. .far asks for column 4. In a normal grid that would create implicit columns 3 and 4; in a subgridded direction there are none, so the item is clamped into the subgrid's last column, measured 60px in (parent column 2).
  3. .near is auto-placed in column 1 of the subgrid, which is parent column 1: 0px. If the subgrid started at parent column 2, its line 1 would be parent line 2; numbering is always local.
  4. The second subgrid has padding-left: 10px. The tracks still come from the parent, but the item in the first track is inset by the padding: measured at 10px and 40px wide instead of 50px. The item in the second track is untouched (60px, 50px wide).

#Grid or flexbox?

Added

Both lay out a parent's children, both have gap and the same alignment properties, and many layouts can be built either way. The difference is how many directions the layout controls. Flexbox is one-dimensional: it arranges items along a line, and if they wrap, each line is laid out on its own, knowing nothing about the line above. Grid is two-dimensional: rows and columns exist first, and items are placed into them, so things line up both ways.

A second way to put it: flexbox is content-out, grid is layout-in. In flexbox, item sizes start from their content (flex-basis, then grow and shrink), so it's great when the items decide the layout: a toolbar of buttons of different widths, tags that wrap, a nav with one item pushed to the end. In grid you define the tracks and the content fills them, so it's great when the layout decides: a page shell, a card gallery with equal columns, a form with aligned labels.

The difference is easiest to see when items wrap:

Five items wrapping: flex-wrap vs auto-fill grid
CSS
.row { gap: 10px; margin-bottom: 12px; }
.flex { display: flex; flex-wrap: wrap; }
.flex > div { flex: 1 1 150px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.row > div { height: 36px; background: #dbeafe; color: #1e3a8a; padding: 0 6px; }
.grid > div { background: #dcfce7; color: #14532d; }
HTML
<div class="row flex"><div>flex 1</div><div>flex 2</div><div>flex 3</div><div>flex 4</div><div>flex 5</div></div>
<div class="row grid"><div>grid 1</div><div>grid 2</div><div>grid 3</div><div>grid 4</div><div>grid 5</div></div>
Resultlive, in your browser
What’s happening
  1. Both containers are 616px wide (the default 640px test frame minus margins), with 10px gaps, and both items ask for about 150px.
  2. Flexbox fills a line with as many 150px bases as fit: three (3 × 150 + 20 = 470, a fourth would need 630). Each line then grows on its own: line 1's three items share 616 − 20 → 198.7px; line 2 has only two items, which grow to (616 − 10) / 2 = 303px.
  3. Grid decides the columns first: floor((616 + 10) / 160) = 3 tracks of 198.7px. Items 4 and 5 drop into the same columns on row 2, so they stay 198.7px and line up under items 1 and 2, leaving an empty cell.
  4. Neither is wrong. If the last line should spread out (tags, buttons), that's flexbox. If items should form columns (a product grid), that's grid. Trying to make flexbox do columns usually ends in calc() widths and negative margins, and trying to make grid do content-sized wrapping ends in auto-fit tricks that still can't size each item to its content.
AdvancedFlexbox vs grid at a glance
Reach forWhen
FlexboxOne row or one column; items sized by their content; spacing items out along a line (justify-content: space-between, margin-left: auto); wrapping where each line can differ; centring one thing
GridRows and columns that must line up; a page skeleton (header, sidebar, main); equal-width card galleries; overlapping items without position; aligning the insides of sibling components (subgrid)
BothMost real pages: grid for the page and the gallery, flexbox inside each card and in the toolbar
AdvancedWhat neither does yet: masonry

A Pinterest-style masonry layout (columns where each item tucks up under the shortest item above, no row lines) isn't possible with grid or flexbox today without JavaScript or CSS multi-column (which orders items down each column instead of across). The CSS Working Group settled on a new display type, display: grid-lanes, specified in CSS Grid Level 3; as of October 2026 the web-features data lists it as not supported in any stable browser, and Chrome 155 (what these tests run in) rejects it:

Is grid-lanes supported here?
CSS
.box { padding: 8px; background: #fee2e2; color: #7f1d1d; }
@supports (display: grid-lanes) {
  .box { background: #dcfce7; color: #14532d; }
  .box::after { content: " (grid-lanes supported)"; }
}
HTML
<div class="box">This box turns green where display: grid-lanes is supported</div>
Resultlive, in your browser
What’s happening
  1. @supports (display: grid-lanes) applies its rules only where the browser can parse that declaration.
  2. In Chrome 155 CSS.supports("display", "grid-lanes") is false, so the box stays red (rgb(254, 226, 226)). Open this page in a browser that has shipped it and the live result turns green: that's the progressive-enhancement pattern for any new layout feature.
Which one?

Pick grid or flexbox for each: (1) a header bar with a logo on the left and three links pushed to the right; (2) a photo gallery where every photo is the same width and rows line up; (3) a list of tags of different lengths that wraps onto several lines.

Show answer

(1) Flexbox, (2) grid, (3) flexbox.

What’s happening
  1. The header is a single line of content-sized items with space between groups: display: flex; align-items: center plus margin-left: auto on the first link (or justify-content: space-between with the links in a group). One dimension, sized by content.
  2. The gallery needs columns that line up across rows: repeat(auto-fill, minmax(200px, 1fr)) gives equal columns that adapt to the width, as measured in this topic's main example.
  3. Tags should each be as wide as their text and wrap like words: display: flex; flex-wrap: wrap; gap: 8px. In a grid every tag would be forced to a column width.

Written fresh for interviews (there was no Google Doc for this one). Every live example was checked in headless Chrome 155 by scripts/check-demos.mjs.

RohitDownloads · All chapters

Sync progress across devices

Type the same private phrase on your Mac and your phone, and your Learned ticks follow you between them — on every study site.

The phrase never leaves this device: only a fingerprint of it is sent, and the server stores a fingerprint of that. Anyone who knows the phrase could see or change your ticks, so pick something you don’t use elsewhere.

Sync is on in this browser.

Sync ID

This ID must be the same on every device. If another device shows a different one, its phrase is different (capital letters count): tap “Turn off here” on it and type the phrase again exactly.