Overview
Grid is the layout primitive for two-dimensional work: rows and columns that line up. Use it for page shells, dashboards, card galleries, and form rows where labels and inputs align across multiple rows. The umbrella overview lives in css; this page is the depth for the cases where grid earns its keep.
Use grid for 2D, flex for 1D
Pick by the dimension count, not by familiarity. A single row or column with items that grow and shrink is flex territory; see css-flexbox. Anything with both axes locked, or alignment that must survive across rows, is grid.
.page {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
min-block-size: 100dvh;
}Nesting flex inside grid inside flex is normal. Mix freely; do not force one primitive to do both jobs.
Size tracks with fr, minmax(), and auto-fill
Avoid pixel-perfect grids. Use intrinsic sizing so the layout absorbs content variance.
1frdistributes free space after intrinsic sizes resolve.minmax(min, max)clamps a track so it never collapses or runs away.repeat(auto-fill, minmax(16rem, 1fr))makes a responsive card grid in one line.
.cards {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
}Use auto-fit instead of auto-fill only when you want surviving items to stretch and fill the row. The two read the same and behave differently at the last row.
Reach for subgrid when children must align with the parent
Subgrid lets a nested grid inherit the parent’s tracks. Use it when card internals (title, body, footer) need to line up across cards regardless of content height.
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto 1fr auto;
}
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
}Subgrid ships in every evergreen browser as of 2025. Use it instead of brittle row-height hacks or JavaScript that measures and rewrites styles.
Name lines and areas; do not count columns
Grid lines numbered 1, 2, 3 are unreadable two months later. Name the lines, or name the areas.
.page {
display: grid;
grid-template-columns: [sidebar] 240px [content-start] 1fr [content-end];
grid-template-areas:
"sidebar header"
"sidebar main"
"sidebar footer";
}
.header {
grid-area: header;
}
.main {
grid-area: main;
}Named areas survive refactors. A new column inserted in the middle reorders by name, not by index, and the call sites do not change.
Separate layout from content
The grid container owns the layout. Children own their content. Do not put width, margin-left: auto, or flex on grid items that already participate in tracks.
/* Wrong: fighting the grid with content rules. */
.card {
width: 320px;
margin-left: auto;
}
/* Right: let the grid size the track. */
.cards {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}This separation is what makes a grid auditable. The container shows the layout; the items show the content. Pair it with css-custom-properties tokens for spacing and the grid stays editable in one place.
Use gap, not margins
Grid (and flex) have first-class gutters. Use gap, row-gap, and column-gap. Margins on children leak through nested layouts and break with gap-aware utilities. See tailwind for utility-class equivalents (gap-4).
Common pitfalls
- Items overflowing tracks. A child with
min-width: auto(the default) refuses to shrink below its content. Setmin-width: 0on grid items that should compress. - Counting numeric lines. Lines 1, 2, 3 break the moment you add a column. Name lines or areas.
- Mixing fixed and
frtracks withoutminmax(). A200px 1frgrid collapses the1frtrack when content overflows. Useminmax(0, 1fr). - Hiding tabbed elements with
display: nonefor layout reasons. Hidden grid items skip the tab order; see accessibility for the keyboard rule. - Reordering items with
orderorgrid-row-start. Visual order and DOM order desync; screen readers follow the DOM.