You write three equal columns, load the page, and one of them refuses to shrink. A scrollbar appears along the bottom of the window. The card that caused it holds a deployment URL — one long unbreakable string — and that string is now setting the width of your entire dashboard.
Nothing in the stylesheet mentions that URL. You asked for repeat(3, 1fr), which reads like “three equal thirds”, and the browser delivered something else without comment.
This isn’t a bug, and grid isn’t being clever behind your back. It’s the thing nobody tells you on day one: you are describing tracks, and tracks have opinions about their contents. The fix is four characters long. The reason it works is the part worth keeping, because the same reason explains uneven rows, stretched cards, and the off-by-one you keep hitting when you place things by hand.
The Column That Won’t Shrink
Here’s the layout. A dashboard, one card per project, three across:
.dashboard {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
Three cards go in, three equal columns come out. That holds right up until one card contains something the browser can’t make narrower — a deployment URL, a git SHA, a pre block, a small table, any run of text with white-space: nowrap.
Then the failure arrives in two stages. First the other two columns give up their space: they get narrower, and your equal thirds quietly stop being equal. Then, when there’s nothing left to give, the grid stops fitting inside its container and the page scrolls sideways.
The card never asked to be wide. The track asked on its behalf, using a rule you didn’t write and can’t see in your stylesheet.
The reflex fix is overflow: hidden on the card, and it works — which is unfortunate, because it teaches the wrong lesson. It happens to disable the rule that caused this, and we’ll get to why. But if you stop there, the next grid you write breaks the same way and you’ll reach for the same hammer.
You Size Tracks, Not Items
display: grid turns an element into a grid container, and nearly everything you write afterwards describes that container’s skeleton rather than any particular child. grid-template-columns defines a row of tracks. Items are placed into the cells those tracks make, and their properties choose which cells — not how wide the cells are.
If you learned layout by putting widths on things, this is the inversion. A grid item’s width is a consequence of the track it landed in. Setting width: 50% on a grid item measures against its cell, which is usually not what you wanted and always confusing to debug.
The other half of the model is counting. Three tracks means four lines, because lines are the edges. Line 1 is the left edge of the first track, line 4 is the right edge of the third.
Fig. 02 — Four lines, three tracks. Line 1 to line 3 covers the first two.
So a summary card spanning the first two columns is written against lines, and the number that looks wrong is the right one:
.summary {
grid-column: 1 / 3;
}
If counting edges makes your head hurt, grid-column: span 2 says the same thing in tracks and lets the browser do the arithmetic. And -1 is the last line, so grid-column: 1 / -1 means full width regardless of how many tracks there turn out to be.
Fr Is Not A Fraction
fr distributes leftover space. Given 240px 1fr 1fr with a 1rem gap, the browser takes the container’s width, subtracts the fixed track and both gaps, and splits whatever remains between the two flexible tracks. Widen the fixed column and both fr columns shrink to pay for it.
Which means 1fr 2fr is not “a third and two thirds of the container”. It’s a third and two thirds of what’s left over. With no fixed tracks and no gaps those two readings give the same answer, and that coincidence is why the fraction reading survives for so long before it bites.
Now the part that explains the dashboard. A track sized 1fr is shorthand for minmax(auto, 1fr), and that auto is a minimum, not a starting point. It resolves to the item’s min-content contribution — informally, the widest thing inside it that can’t be broken across lines.
Read the real rule out loud: take your share of the free space, unless your content is wider than that, in which case take what you need. A long URL has no break opportunities, so its min-content width is the whole string, so the track cannot go below it. The track wins its argument with your stylesheet because you never gave the stylesheet an argument to make.
An fr track is a share of what’s left, with a floor you didn’t set.
— Layout_Doctrine // §2
You set that floor yourself with minmax(0, 1fr). Same maximum, explicit minimum of zero, and the column now holds its third no matter what’s inside it.
The content still has to go somewhere, and that’s the trade: grid stops protecting it, so the card needs to say what happens instead. overflow-wrap: anywhere breaks the URL mid-string. overflow-x: auto gives that one card its own little scrollbar. Either is a decision; the overflowing page was not.
.dashboard {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.card__url {
overflow-wrap: anywhere;
}
This is also why overflow: hidden on the card appeared to fix things. A grid item whose overflow is anything other than visible gets an automatic minimum size of zero — the spec assumes a scroll container can manage its own contents. The floor vanishes as a side effect of a property you set for an unrelated reason, which is a fine way to fix a bug and a terrible way to understand one.
| Declaration | Smallest the track can get | Reach for it when |
|---|---|---|
1fr | The widest unbreakable content | Content is short and should win ties |
minmax(0, 1fr) | Zero | The ratio matters more than the content |
minmax(12rem, 1fr) | 12rem, then it overflows | Cards that stop being readable squeezed |
The Implicit Grid Fills In
You declared columns. You never declared a single row, and yet rows exist. Anything grid needs that you didn’t ask for, it creates implicitly — extra rows to hold the items that didn’t fit in the explicit ones, sized auto by default.
Two visible consequences follow. An auto row is as tall as its tallest item, and items stretch to fill their cell in both directions unless told otherwise. So one card with three extra lines of text makes two neighbours tall and mostly empty. That’s align-items: stretch doing exactly its job, and align-items: start is the off switch.
grid-auto-rows sets the template for every implicit row, not just the first, which is the one people expect it to affect:
.dashboard {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-auto-rows: minmax(9rem, auto);
gap: 1rem;
}
The implicit grid is also why manual placement never throws. Put grid-row: 7 on an item in a two-row grid and nothing errors — grid builds rows three through seven, leaves them empty, and drops your item exactly where you asked, six rows of whitespace and all.
One thing to be careful with: grid-auto-flow: dense lets later items backfill holes left by earlier ones, and it changes only the visual order. DOM order is unchanged, so tab order and screen-reader order stay in source order. That’s acceptable for a gallery of independent tiles and wrong for anything a