CSS Grid is the only native CSS layout system designed to handle rows and columns simultaneously — Flexbox, by contrast, is fundamentally one-dimensional, even when it wraps. That distinction is why Grid is the right tool for page-level layout (headers, sidebars, footers) and dashboards, while Flexbox stays better suited to distributing items along a single axis, like a toolbar or a nav.
Grid has its own terms, and half the confusion around it comes from not knowing them:
grid-template-columns/rowsgrid-column: 2 / 4)grid-template-*grid-auto-rows/columnsgrid-template-areas lets you literally draw the layout in CSS as ASCII art, which makes the relationship between markup and visual position obvious at a glance — no counting line numbers.
.page {
display: grid;
grid-template-columns: 220px 1fr;
grid-template-rows: 64px 1fr 56px;
grid-template-areas:
"sidebar header"
"sidebar main"
"sidebar footer";
min-height: 100vh;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }
Change the layout — say, moving the sidebar to a bottom nav on mobile — by redefining grid-template-areas inside a media query, without touching a single line of HTML.
Both go inside repeat() with minmax() and both create a responsive grid without media queries — but they behave differently when there isn't enough content to fill a row, and that difference trips up almost everyone the first time.
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
/* vs: repeat(auto-fill, minmax(200px, 1fr)); */
gap: 16px;
}
| Keyword | With 2 items in a 4-column-wide space |
|---|---|
auto-fit | Collapses empty tracks — the 2 items stretch to fill the row |
auto-fill | Keeps empty tracks in place — the 2 items stay their minmax() width, leaving visible gaps |
For a card grid where items should stretch to use available space, use auto-fit. For something like a calendar grid where consistent cell width matters more than filling the row, auto-fill is usually correct.
UIXDraft's 180+ template bundle includes dashboard, portfolio, and landing page layouts built entirely with modern CSS Grid — inspect the source and adapt it directly.
See the templates →Before subgrid, a card inside a grid couldn't align its internal rows (title, body, footer) with the cards next to it — each card's grid was isolated from its parent. Subgrid lets a nested grid inherit its parent's tracks:
.card-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3; /* must span enough rows to inherit from */
}
With subgrid, if one card's title wraps to two lines, every card's body and footer shift down together to stay aligned — something that used to require matching heights with JavaScript.
When grid items span different numbers of columns or rows, the default placement algorithm leaves gaps rather than backtrack to fill them — it only ever moves forward. For a masonry-style gallery where visual gaps look broken, dense packing tells the browser to go back and fill holes with later items:
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
grid-auto-flow: dense;
gap: 12px;
}
.gallery .featured {
grid-column: span 2;
grid-row: span 2;
}
Be aware that dense can reorder items visually away from their source order in the DOM — fine for a photo gallery, but a poor choice for anything where reading order matters, like a list of articles, since screen readers still follow DOM order.
When more items exist than your explicit grid-template-rows accounts for, the browser creates implicit rows using whatever default sizing applies — usually auto, which can produce inconsistent row heights. Control it explicitly:
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: minmax(120px, auto);
gap: 16px;
}
This guarantees every implicitly-created row is at least 120px tall but can still grow with content — the same minmax() pattern used for columns, just applied to the axis that grows automatically.
| Question | Answer points to |
|---|---|
| Do rows and columns need to align with each other? | Grid |
| Are you laying out items along a single line that may wrap? | Flexbox |
| Is content driving the size, or is the layout driving content placement? | Content-driven → Flexbox. Layout-driven → Grid |
In practice, most real interfaces use both: Grid for the page skeleton, Flexbox for aligning items inside individual components like a card's icon-plus-label row.
Named areas are the most readable option, but sometimes precise numeric placement is faster — especially for items that need to overlap or break out of the normal flow, like a badge overlapping a card's corner:
.hero-image {
grid-column: 1 / 3;
grid-row: 1 / 2;
}
.badge {
grid-column: 2 / 3;
grid-row: 1 / 2;
justify-self: end;
align-self: start;
transform: translate(8px, -8px);
}
Negative line numbers count from the end of the grid instead of the start, which is useful when the total track count might change: grid-column: 1 / -1 always spans the full width regardless of how many columns exist.
min-width: 0 on grid children — text or a wide child can overflow its track because grid items default to min-width: auto, refusing to shrink below their content size.margin instead of gap for spacing — gap only adds space between tracks, avoiding the extra edge margin you'd otherwise have to cancel out with negative margins.grid-template-areas requires every area to form a contiguous rectangle; an L-shaped area is invalid and silently ignored.Usually because the grid items themselves have collapsing margins or a background that visually masks the gap, or because gap was applied to a flex container in a browser old enough to only support it on grid (support has been universal on both since 2021, so this is now rare).
Track sizes defined in fixed units (px) can transition, but mixed units like 1fr historically couldn't interpolate smoothly. Modern browsers now support animating grid-template-columns and rows directly in most cases — test in your target browsers, and fall back to animating a wrapping element's width if you see jumps.
Grid has had full support in all major browsers since 2020, including on mobile. The only realistic exception is if you must support IE11, which never got unprefixed Grid — in that case use @supports (display: grid) to provide a Flexbox fallback. For any project targeting current browsers, this is no longer a practical concern worth designing around.