minmax() and the fr unit explained
The fr unit and minmax() function are the building blocks of flexible grid layouts. Together they let tracks grow, shrink, and respect constraints — all without media queries.
What the fr unit does
The fr (fraction) unit represents a share of the remaining free space in the grid container after all fixed-size tracks and gaps have been accounted for. It does not represent a fraction of the total container width.
/* Container is 1200px, gap is 1rem (16px) */
.grid {
display: grid;
grid-template-columns: 200px 1fr 2fr;
gap: 1rem;
}
/*
Fixed space: 200px + 2 gaps (32px) = 232px
Free space: 1200 - 232 = 968px
1fr = 968 / 3 = ~323px
2fr = 968 × 2/3 = ~645px
*/
fr tracks never shrink below their content's minimum width by default. This is because fr behaves like minmax(auto, 1fr) — the implicit minimum is auto, which resolves to the content's minimum size.
The minmax() function
minmax(min, max) defines a track that is at least min wide and at most max wide. The browser chooses a size within this range based on available space and content.
/* Sidebar: at least 12rem, at most 20rem */
.layout {
grid-template-columns: minmax(12rem, 20rem) 1fr;
}
/* Cards: at least 250px, grow to fill space */
.cards {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
/* Fixed minimum, flexible maximum */
.tracks {
grid-template-columns:
minmax(100px, 1fr)
minmax(200px, 2fr)
minmax(100px, 1fr);
}
minmax() with auto
Using auto as either argument gives content-aware sizing. As a minimum, auto resolves to the largest minimum size of the content (like the longest word). As a maximum, auto resolves to max-content — the size needed to show all content without wrapping.
/* auto minimum — track won't squash content */
grid-template-columns: minmax(auto, 300px);
/* auto maximum — track grows to fit content, no limit */
grid-template-columns: minmax(200px, auto);
/* Both auto — equivalent to just "auto" */
grid-template-columns: minmax(auto, auto);
/* Same as: grid-template-columns: auto; */
/* Common pattern: content-aware minimum, flexible max */
.sidebar-layout {
grid-template-columns: minmax(auto, 16rem) 1fr;
/* Sidebar never squashes its content,
but caps at 16rem */
}
Why fr !== percentage
A common mistake is treating 1fr as equivalent to a percentage. They are fundamentally different: percentages are based on the container's total width, while fr divides only the leftover space. This distinction matters when you mix fixed and flexible tracks.
/* Container: 1000px, gap: 20px */
/* Percentages — ignore gaps, can overflow */
grid-template-columns: 30% 70%;
/* 300px + 700px = 1000px, plus 20px gap = 1020px overflow! */
/* fr — accounts for gaps automatically */
grid-template-columns: 3fr 7fr;
/* Free space: 1000 - 20 = 980px
3fr = 294px, 7fr = 686px. Total + gap = 1000px. Perfect. */
/* Mixing fixed + percentage can overflow */
grid-template-columns: 200px 50%;
/* 200px + 500px + 20px gap = 720px — no overflow here,
but the percentage doesn't account for the 200px column */
/* Mixing fixed + fr — always fits */
grid-template-columns: 200px 1fr;
/* 1fr gets 1000 - 200 - 20 = 780px. Always totals 1000px. */
fr over percentages in grid layouts. The fr unit naturally accounts for gaps and fixed tracks, preventing overflow.The minmax(0, 1fr) trick
Since 1fr behaves like minmax(auto, 1fr), tracks won't shrink below their content. This can cause overflow when content is wide (long URLs, code blocks, images). Override the implicit minimum with minmax(0, 1fr) to allow tracks to shrink below content size.
/* Problem: 1fr column won't shrink below content */
.layout {
grid-template-columns: 200px 1fr;
}
/* If the 1fr column has a 2000px image,
it overflows the container */
/* Solution: allow the track to shrink to 0 */
.layout {
grid-template-columns: 200px minmax(0, 1fr);
}
/* Now the 1fr column can shrink, and overflow
is handled by the content (overflow: hidden, etc.) */
/* Combine with overflow handling on the child */
.layout__content {
min-width: 0; /* allow flex/grid children to shrink */
overflow: hidden; /* clip overflowing content */
}
Practical patterns
Here are battle-tested patterns that combine minmax() and fr for common layouts.
/* Responsive card grid — no media queries */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: 1.5rem;
}
/* Holy grail layout with constrained sidebar */
.page {
display: grid;
grid-template-columns:
minmax(10rem, 16rem)
minmax(0, 1fr)
minmax(10rem, 20rem);
gap: 2rem;
}
/* Full-bleed layout with centered content */
.full-bleed {
display: grid;
grid-template-columns:
minmax(1rem, 1fr)
minmax(0, 65ch)
minmax(1rem, 1fr);
}
.full-bleed > * { grid-column: 2; }
.full-bleed > .bleed { grid-column: 1 / -1; }
The min(100%, 18rem) inside minmax() prevents the minimum from exceeding the container on very small screens — a robust technique for truly responsive grids.