In CSS Grid, how does the fr unit differ from a percentage track size such as 33.333% in grid-template-columns?
answer
- leftover space, not total space
- gutters come off the pool first
- percentages never saw the gap
- flex factors normalise against their own sum
basics
~20 sThe fr unit is a share of the leftover free space, computed after fixed tracks, gaps and content minimums are taken out, while a percentage is a share of the container's full content-box width and ignores gaps entirely — which is why percentage columns plus a gap overflow.
solid answer
~50 sA percentage track resolves against the grid container's content-box size, so `repeat(3, 33.333%)` asks for the entire width three times over — and because `gap` is added on top of the tracks rather than taken out of them, adding `gap: 20px` pushes the grid 40px past its container. The `fr` unit works the other way around: the browser first removes fixed tracks, gutters and any content-driven minimums, and then distributes what is *left* in proportion to the flex factors, so `repeat(3, 1fr)` with `gap: 20px` in a 900px container gives three 286.67px columns and fits exactly. Flex factors are normalised against their own sum, so `2fr 1fr 1fr` is a half/quarter/quarter split, not a fraction of some fixed whole. Two practical consequences: `fr` cannot be used inside `calc()`, and a percentage in a container with an indefinite size behaves as `auto`.
code
css · 11 lines.percent {
display: grid;
grid-template-columns: repeat(3, 33.333%);
gap: 20px;
}
.fraction {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}go deeper
Know the headline: fr splits the space that is left over, percentages split the whole container. Be able to say that three 33.333% columns plus a gap will overflow while three 1fr columns will not.
Walk through the algorithm in order — size non-flexible tracks, subtract gutters, then divide the remainder by the sum of the flex factors. Mention that 2fr 1fr 1fr is a half/quarter/quarter split of the free space, not of the container.
Show you can pick between them deliberately: fr when the layout should absorb whatever is left, a percentage when a track's width must not depend on sibling content. Point out that a bare 1fr has an auto minimum, so an fr grid can still overflow.
Frame it as removing arithmetic from the codebase: fr-based track lists survive sidebar and gutter changes without touched breakpoints, which is a maintenance argument, not a stylistic one. Be ready to justify where percentage tracks still earn their place.
## Two different denominators Both units look like "a fraction of the row", but they divide different quantities. A **percentage** track resolves against the grid container's content-box size in that axis. `33.333%` of a 900px content box is 300px, full stop — it is computed before the browser knows anything about gutters or siblings. An **fr** value is not a length at all. It is a *flex factor*: an instruction to take a share of the **free space**, which the grid track sizing algorithm defines as the space remaining after every non-flexible track has been sized and every gutter has been subtracted. ## The gap trap This is the difference interviewers actually probe, because it produces a visible bug. ```css /* 900px container: three 300px columns + two 20px gaps = 940px → overflows */ .percent { display: grid; grid-template-columns: repeat(3, 33.333%); gap: 20px; } /* 900px container: (900 − 40) / 3 = 286.67px each → fits exactly */ .fraction { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; } ``` `gap` creates gutters *between* tracks; it does not shrink them. Percentage tracks were sized without any knowledge of those gutters, so the sum overshoots. The classic float/percentage-era workaround was to subtract the gutter by hand, or to fake gutters with negative margins on the container. With `fr` there is no arithmetic to do, because the gutters are removed from the pool before the split happens. ## How the split is actually computed The algorithm, in the order that matters for interviews: 1. Size every non-flexible track (lengths, percentages, content-based tracks). 2. Subtract those track sizes and all gutters from the grid container's inner size. What remains is the free space. 3. If the flex factors sum to **1 or more**, divide the free space by the sum and give each flexible track `factor × share`. So `2fr 1fr 1fr` is 50%/25%/25% of the free space. 4. If the flex factors sum to **less than 1**, each track gets only `factor × free space` and the remainder stays unused — `0.5fr 0.25fr` deliberately leaves a quarter of the free space empty. Because of step 3, flex factors are relative to each other, never to some fixed total. Changing `1fr 1fr` to `100fr 100fr` changes nothing. ## fr has a floor, percentages do not A bare `1fr` is shorthand for `minmax(auto, 1fr)`. That `auto` minimum is the content's automatic minimum size, so a flexible track will refuse to shrink below what its content needs — an `fr` grid can still overflow when the content is wider than its share. A percentage track has no such protection in the other direction: it takes its computed length whether the content fits or not. If you want a genuinely floor-free flexible track, write `minmax(0, 1fr)` explicitly. ## Other practical differences - **`fr` is not usable in `calc()`.** `calc(1fr - 20px)` is invalid; the whole point of `fr` is that the subtraction has already happened. - **Percentages need a definite container size.** If the grid container's size in that axis is indefinite — a column-axis percentage in an auto-height grid, for instance — the percentage behaves as `auto`, which is a common source of "my percentage row did nothing". - **`fr` only exists in grid track sizing.** It is not a general length unit; you cannot write `width: 1fr`. - **Mixing is fine and often best.** `240px 1fr` is a fixed sidebar plus a main column that absorbs everything left, and it needs no arithmetic when the sidebar width changes. ## When a percentage is still the right choice Percentages are not obsolete in grid. When you want a track to be a share of the *container* regardless of how much content the other tracks demand — a fixed 25% rail whose width must not depend on siblings' content — a percentage says that directly, where `fr` would let content pressure change the split. Just remember to account for the gutters yourself, typically by keeping the percentages summing to less than 100% or by sizing only some tracks that way. ## The one-line answer to memorise `fr` divides what is left; `%` divides the whole. Everything else — the gap overflow, the normalisation of flex factors, the inability to use `fr` in `calc()` — follows from that sentence.
- With grid-template-columns: 2fr 1fr 1fr, what fraction of the container does the first column get?Not necessarily a half of the container — a half of the *free space*. The flex factors sum to 4, so the first track takes 2/4 of whatever remains after fixed tracks, gutters and content minimums are removed. If there are no other tracks and no gap, that happens to be 50% of the container; add a 40px gap and it is 50% of (width − 40px).
- What happens when the flex factors in a grid sum to less than 1, for example 0.5fr 0.25fr?Each track gets only its literal fraction of the free space rather than a normalised share, so the tracks take 50% and 25% of the leftover and the remaining 25% is simply left empty. It is a deliberate feature for reserving slack, and it is why sub-1 factors behave so differently from factors that sum above 1.
- Can you write calc(1fr - 20px) as a track size?No — `fr` is not a length and is not allowed inside `calc()`. The subtraction it implies is already built into the algorithm, which removes gutters and fixed tracks before distributing free space. If you need a track that is the remainder minus something, size the something as its own track or use padding on the item instead.
saying these in an interview costs you the question
- Says 1fr is just shorthand for an equal percentage
- Expects three 33.333% columns plus a gap to fit
- Thinks gap is subtracted from percentage tracks too
- Believes flex factors are fractions of a fixed whole
- Tries to use fr inside calc() or on width