Before CSS Grid, building a proper page layout meant floats, clearfixes, negative margins, and a prayer that your columns didn't collapse. Flexbox helped, but it's fundamentally one-dimensional — you get a row or a column, not both. CSS Grid is the first layout system native to CSS that's actually designed for two-dimensional layouts. The challenge isn't browser support — it's been universal since 2017. The challenge is that the API has a lot of surface area and the naming is inconsistent enough to be confusing if you don't have the underlying model clear. This article focuses on the parts you'll use in real work.
Rows, Columns, and the Grid Container
Everything starts with display: grid on the container. Once set, the direct children become grid items automatically — no class needed on them. You define the column and row tracks with grid-template-columns and grid-template-rows.
The key unit to understand first is fr — the fractional unit. It represents a fraction of the available space in the grid container after fixed-size tracks have been accounted for. One 1fr means "take one share of whatever space is left." Two columns of 1fr 1fr means equal halves. 2fr 1fr means the first column gets twice as much as the second. This is the unit that makes fluid grids feel natural.
/* 3-column blog card layout */
.card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto;
gap: 1.5rem;
}
/* repeat(3, 1fr) is shorthand for: 1fr 1fr 1fr */
/* auto rows size to their content */repeat(n, track) is a shorthand so you don't have to write the same track size five times. It also works with mixed values: repeat(3, 1fr 2fr) gives you a 6-column grid alternating narrow and wide tracks. For rows, auto is almost always what you want — rows size to their content unless you explicitly need a fixed height.
<div class="card-grid">
<article class="card">
<img src="/posts/grid-post-1.jpg" alt="CSS Grid post 1">
<h2>Getting Started with Grid</h2>
<p>A practical intro for developers moving from Flexbox.</p>
</article>
<article class="card">
<img src="/posts/grid-post-2.jpg" alt="CSS Grid post 2">
<h2>Grid vs Flexbox</h2>
<p>Which layout tool to reach for, and when.</p>
</article>
<article class="card">
<img src="/posts/grid-post-3.jpg" alt="CSS Grid post 3">
<h2>Responsive Grids Without Media Queries</h2>
<p>Using minmax() to handle reflow automatically.</p>
</article>
</div>The gap Property
Before Grid, adding spacing between layout children meant margins — and margins meant remembering to reset the last child, dealing with collapsing, and adding negative margins on the container to compensate. gap solves this cleanly. It adds space between tracks only, never on the outer edges of the grid.
/* Uniform gap between all rows and columns */
.card-grid {
gap: 1.5rem;
}
/* Different row and column gaps */
.dashboard {
row-gap: 2rem;
column-gap: 1rem;
}
/* gap is shorthand: row-gap then column-gap */
.dashboard {
gap: 2rem 1rem;
}gap also works in Flexbox (it replaced the old grid-gap alias). If you're already using Flexbox for a component and just need spacing between items, you can use gap there too without switching to Grid.Placing Items Explicitly
By default, grid items flow into the next available cell in source order. But Grid's real power is that you can place items anywhere — and even make them span multiple tracks. The properties are grid-column and grid-row, which reference grid lines (the lines between tracks, numbered from 1).
/* Dashboard where the first card spans 2 columns */
.dashboard {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
}
.card--featured {
grid-column: 1 / 3; /* start at line 1, end at line 3 */
}
/* span keyword: simpler when you don't care about exact position */
.card--featured {
grid-column: span 2; /* occupy 2 column tracks from wherever it lands */
}
/* Spanning rows too */
.card--tall {
grid-row: span 2; /* occupy 2 row tracks */
}
/* Explicit positioning: place a panel in column 3-5, row 1-2 */
.sidebar {
grid-column: 3 / 5;
grid-row: 1 / 2;
}Line numbers start at 1 for the left/top edge and go up to n+1 where n is the number of tracks. Negative line numbers count from the right/bottom: grid-column: 1 / -1 stretches an item across the full width of the grid regardless of how many columns there are — a useful trick for full-width elements inside a multi-column container.
grid-template-areas — Name Your Layout
This is the feature that makes Grid genuinely great for page-level layouts, and it's the most readable API in all of CSS. Instead of placing items by line numbers, you draw the layout as ASCII art in the CSS, then tell each item which named area it occupies. The grid-template-areas spec requires every row to have the same number of cells, and each named area must be rectangular — no L-shapes.
/* Full page layout: header, sidebar, main, footer */
.page-shell {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: 60px 1fr 50px;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
min-height: 100vh;
gap: 0;
}
.page-header { grid-area: header; }
.page-sidebar { grid-area: sidebar; }
.page-main { grid-area: main; }
.page-footer { grid-area: footer; }<div class="page-shell">
<header class="page-header">Site header / nav</header>
<nav class="page-sidebar">Sidebar navigation</nav>
<main class="page-main">Primary content</main>
<footer class="page-footer">Footer</footer>
</div>A period (.) in the template string leaves a cell intentionally empty. You can also stack the same name across multiple cells to span them — that's exactly what "header header" does above. When you resize or restructure the layout for different viewpoints, you update the template string in one place rather than hunting across multiple grid-column declarations.
auto-fill vs auto-fit with minmax()
The pattern behind responsive grids with zero media queries is one of Grid's most practical tricks: repeat(auto-fill, minmax(250px, 1fr)). Break it down — minmax(250px, 1fr) says each column should be at least 250px wide but expand to fill available space. auto-fill tells Grid to create as many columns as will fit in the container given that constraint. The result is a grid that automatically gains and loses columns as the viewport changes.
/* Product card grid — reflows automatically, no media queries */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1.5rem;
}
/* auto-fit collapses empty tracks — items stretch to fill the container */
.product-grid--stretch {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}The difference between auto-fill and auto-fit is subtle but matters when there are fewer items than columns. With auto-fill, Grid reserves space for the potential tracks even if empty — items stay at their minimum width. With auto-fit, empty tracks collapse to zero and the items expand to fill the row. For product grids and galleries where you want items to use available space, auto-fit usually looks better. For grids where consistent item width matters more than filling the container, auto-fill is the right choice.
auto-fill and auto-fit with minmax() have excellent support — 97%+ globally according to caniuse. You can use this pattern today without a fallback in any modern project.Alignment in Grid
Grid uses the same Box Alignment module as Flexbox, so the property names map directly if you already know Flexbox. The key mental model: justify operates on the inline axis (horizontal in most writing modes), align operates on the block axis (vertical).
justify-items/align-items— default alignment for all items within their grid cell. Default isstretch, which is why grid items fill their cell automatically.justify-content/align-content— distributes the tracks themselves within the container when the grid is smaller than the container (similar to Flexboxjustify-contenton the flex container).justify-self/align-self— overrides alignment for a single item. Useful when one item needs to be centered while others stretch.- Values:
start,end,center,stretch,space-between,space-around,space-evenly(last three for*-contentonly).
/* Center all card content within its cell */
.card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: start; /* cards align to their cell top, not stretched */
gap: 1.5rem;
}
/* Center a single call-to-action card both ways */
.card--cta {
justify-self: center;
align-self: center;
}
/* Distribute tracks with space between — useful for nav bars */
.nav-grid {
display: grid;
grid-template-columns: auto auto auto;
justify-content: space-between;
}If you're coming from Flexbox, the main difference is that Grid alignment operates on a two-dimensional space — you can control both axes simultaneously. In Flexbox, the cross-axis alignment applies to an entire row or column of items; in Grid, each item has its own cell, so justify-self and align-self give you per-item control without needing wrapper elements.
Real Layout: Complete Dashboard Shell
Here's a complete CSS Grid page structure for a dashboard application — header, collapsible nav sidebar, main content area with its own internal grid, and footer. This is the pattern you'd use as the outermost layout shell for a SaaS dashboard or admin panel.
/* ── Dashboard shell ──────────────────────────────────── */
.dashboard-shell {
display: grid;
grid-template-columns: 220px 1fr;
grid-template-rows: 56px 1fr 44px;
grid-template-areas:
"topbar topbar"
"sidenav content"
"sidenav footer";
min-height: 100vh;
}
.dashboard-topbar { grid-area: topbar; background: #1e293b; }
.dashboard-sidenav { grid-area: sidenav; background: #0f172a; overflow-y: auto; }
.dashboard-content { grid-area: content; padding: 1.5rem; overflow-y: auto; }
.dashboard-footer { grid-area: footer; background: #f8fafc; border-top: 1px solid #e2e8f0; }
/* ── Collapsed sidebar variant ────────────────────────── */
.dashboard-shell--collapsed {
grid-template-columns: 56px 1fr;
}
/* ── Content area — internal card grid ─────────────────── */
.dashboard-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
grid-template-rows: auto;
align-content: start;
gap: 1.25rem;
}
/* Stat cards take one column each; the main chart spans all */
.stat-card { /* auto-placed, one column */ }
.main-chart { grid-column: 1 / -1; } /* always full width */
/* ── Responsive: collapse to single-column on small screens */
@media (max-width: 768px) {
.dashboard-shell {
grid-template-columns: 1fr;
grid-template-rows: 56px 1fr auto 44px;
grid-template-areas:
"topbar"
"content"
"sidenav"
"footer";
}
}<div class="dashboard-shell">
<header class="dashboard-topbar">
<a href="/dashboard" class="brand">AppName</a>
<nav class="topbar-actions">
<button class="btn-icon" aria-label="Notifications">
<span class="icon-bell"></span>
</button>
<img src="/avatar/user.png" class="user-avatar" alt="User menu">
</nav>
</header>
<nav class="dashboard-sidenav">
<ul>
<li><a href="/dashboard">Overview</a></li>
<li><a href="/dashboard/analytics">Analytics</a></li>
<li><a href="/dashboard/users">Users</a></li>
<li><a href="/dashboard/settings">Settings</a></li>
</ul>
</nav>
<main class="dashboard-content">
<div class="stat-card">Monthly Revenue</div>
<div class="stat-card">Active Users</div>
<div class="stat-card">Churn Rate</div>
<div class="main-chart">Revenue Chart (full width)</div>
</main>
<footer class="dashboard-footer">
<p>AppName v2.4.1 — <a href="/status">System Status</a></p>
</footer>
</div>A few things worth noting in this example. The footer is placed in the sidenav column on large screens using the template area definition, which keeps it visually separated from the main content scrolling area. The 1fr on rows and overflow-y: auto on the content pane means the content area scrolls independently — the topbar and sidenav stay fixed in place, which is the standard dashboard UX. The @media query at the bottom reorders the grid areas for mobile without touching the HTML at all.
Wrapping Up
CSS Grid's learning curve comes mostly from the naming — lines, tracks, areas, fr units — but once the model clicks, it's remarkably direct. For real layouts: use grid-template-areas for page shells (it reads like a wireframe), use repeat(auto-fit, minmax()) for responsive card grids, and use explicit grid-column: span N for items that break the standard flow. You don't need all of Grid's API for 90% of real work — these patterns cover most of it.
For further reading: the CSS-Tricks Complete Guide to Grid is the best single-page reference; MDN's CSS Grid documentation goes deep on every property; and the W3C CSS Grid Level 1 spec is surprisingly readable if you want to understand the exact behavior of edge cases. Browser compatibility is excellent — check caniuse.com/css-grid for current numbers. If you're working on a live project and want to clean up your CSS, the CSS Formatter on this site will tidy and prettify your stylesheets.