CSS Grid and Subgrid: A Comprehensive Deep Dive
CSS Grid Layout is the most powerful layout system available in CSS. It provides a two-dimensional grid-based approach to layout design, allowing you to control both columns and rows simultaneously — something flexbox, which operates in a single axis, was never designed to do. Since its introduction in 2017, Grid has become the foundation for page-level layouts, complex component designs, and responsive patterns that previously required elaborate hacks or JavaScript.
Subgrid, the long-awaited companion feature, solves Grid's biggest limitation: nested grids could not align with their parent grid. Before subgrid, a card's internal elements could not participate in the parent grid's track sizing, which meant that card titles, descriptions, and footers in a row of cards would frequently end up at misaligned heights. Subgrid eliminates this problem entirely by allowing a child grid to inherit its parent's track definitions.
This guide covers CSS Grid comprehensively, from the fundamental concepts through advanced patterns, and then dives deep into subgrid. By the end, you will understand not just how to use these features, but when to reach for Grid versus flexbox and how to combine them for optimal layouts.
CSS Grid Fundamentals: Rows, Columns, and Gaps
A grid container is created by setting display: grid on an element. Its direct children become grid items. You define the grid's structure using grid-template-columns and grid-template-rows, and control the spacing between cells with gap.
.grid {
display: grid;
grid-template-columns: 200px 1fr 200px;
grid-template-rows: auto 1fr auto;
gap: 1rem;
}
/* This creates a 3-column, 3-row grid:
- Left and right columns: 200px fixed
- Center column: fills remaining space (1fr)
- Rows: sized to content (auto) except the middle
row which takes remaining vertical space */
The gap property (formerly grid-gap) sets both row-gap and column-gap at once. You can specify them independently: gap: 1rem 2rem sets a 1rem row gap and a 2rem column gap. Gaps create space between tracks but not at the edges of the grid.
grid-template-columns: fr, minmax, auto-fill, auto-fit
The fr unit distributes available space proportionally. 1fr 2fr creates two columns where the second is twice as wide as the first. The fr unit divides the space remaining after fixed-size tracks and gaps are accounted for.
The minmax() function sets a minimum and maximum size for a track. This is essential for responsive layouts where you want columns to have a minimum width but grow if space is available:
/* Columns are at least 250px, but grow equally to fill space */
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(250px, 1fr));
gap: 1.5rem;
}
The repeat() function avoids writing the same track definition multiple times. repeat(3, 1fr) is equivalent to 1fr 1fr 1fr. But the real power comes from combining repeat() with the keywords auto-fill and auto-fit.
The Difference Between auto-fill and auto-fit
Both auto-fill and auto-fit create as many columns as will fit in the container, but they differ in how they handle empty tracks:
/* auto-fill: creates as many tracks as fit, even if empty */
.auto-fill-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
/* auto-fit: collapses empty tracks to zero, expanding items */
.auto-fit-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
With auto-fill, if you have 3 items and the container is wide enough for 5 columns, the grid creates 5 columns — the last 2 are empty but still take up space. With auto-fit, those empty tracks collapse to 0px, and the 3 items expand to fill the entire container width. In practice, auto-fit is what you want most of the time for responsive card grids, while auto-fill is useful when you want items to maintain a consistent size regardless of how many there are.
Named Grid Areas with grid-template-areas
Named grid areas provide a visual, intuitive way to define layouts. You assign names to regions of the grid using grid-template-areas, then place items into those regions with grid-area:
.page {
display: grid;
grid-template-columns: 250px 1fr 300px;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header header"
"sidebar content aside"
"footer footer footer";
min-height: 100vh;
gap: 1rem;
}
.site-header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main-content { grid-area: content; }
.aside { grid-area: aside; }
.site-footer { grid-area: footer; }
/* Responsive: stack vertically on narrow screens */
@media (max-width: 768px) {
.page {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"content"
"sidebar"
"aside"
"footer";
}
}
Each string in grid-template-areas represents one row. Area names must form rectangles — L-shaped or disjointed areas are not valid. A period (.) represents an empty cell. The beauty of this approach is that changing the layout for different breakpoints is as simple as rewriting the area strings; you never need to touch the HTML or move elements around with positioning.
Explicit vs Implicit Grid
The explicit grid is what you define with grid-template-columns, grid-template-rows, and grid-template-areas. If you define 3 columns and 2 rows, your explicit grid has 6 cells. The implicit grid is created automatically when items are placed outside the explicit grid's bounds.
For example, if you define 3 columns but have 9 items, the first 3 items fill the first row (if you only defined one row), and the remaining 6 items cause the browser to create additional rows automatically. The size of these implicit rows is controlled by grid-auto-rows:
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
/* Only one explicit row defined */
grid-template-rows: 200px;
/* Implicit rows will be at least 150px */
grid-auto-rows: minmax(150px, auto);
gap: 1rem;
}
Similarly, grid-auto-columns controls the size of implicitly created columns, and grid-auto-flow determines whether new items are placed in rows (default) or columns. Setting grid-auto-flow: dense enables a packing algorithm that fills in gaps earlier in the grid — useful for masonry-like layouts but potentially confusing for screen readers since visual order may diverge from DOM order.
Placing Items with grid-column and grid-row
By default, grid items are placed in order, filling each cell sequentially. You can explicitly place items using grid-column and grid-row, which specify start and end grid lines:
/* Span the full width (3-column grid) */
.full-width {
grid-column: 1 / -1; /* Line 1 to the last line */
}
/* Span 2 columns starting from column 2 */
.wide-item {
grid-column: 2 / span 2;
}
/* Place in a specific cell */
.specific {
grid-column: 2 / 3;
grid-row: 1 / 2;
}
Grid lines are numbered starting at 1 from the left (for columns) and from the top (for rows). Negative numbers count from the end: -1 is the last line. The span keyword lets you specify how many tracks an item should span rather than an explicit end line.
What Subgrid Is and Why It Exists
Subgrid solves a specific and common problem: aligning the internal structure of nested elements with the parent grid's tracks. Consider a row of three cards, each containing a title, description, and a call-to-action button. Without subgrid, each card is its own independent grid (or flexbox) container. If one card's title wraps to two lines while the others are one line, the descriptions and buttons will be at different vertical positions across the cards.
Subgrid allows a grid item that is itself a grid container to adopt its parent's row or column tracks (or both) instead of defining its own. This means the card's rows align with the parent grid's rows, ensuring consistent alignment across all cards.
.card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
/* Define row tracks for card internals */
grid-template-rows: subgrid; /* Not here - on the children */
gap: 1.5rem;
}
/* Each card spans 3 rows and uses subgrid for alignment */
.card {
display: grid;
grid-row: span 3; /* Card occupies 3 parent rows */
grid-template-rows: subgrid; /* Inherit parent's row tracks */
gap: 1rem;
}
/* Now .card > h2, .card > p, and .card > a
each occupy one of the parent's row tracks,
and they align perfectly across all cards */
Subgrid for Card Content Alignment
Let us expand the card alignment example into a complete, production-ready pattern. This is arguably the most common use case for subgrid and the one that best demonstrates its value:
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
/* The parent grid does NOT use subgrid itself.
It just needs to have row tracks that children
can inherit via subgrid. We use grid-auto-rows
to let rows size to content. */
gap: 1.5rem;
}
.card {
display: grid;
grid-row: span 4; /* title, description, spacer, button */
grid-template-rows: subgrid;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 1.5rem;
}
.card h3 {
margin: 0;
font-size: 1.25rem;
}
.card p {
margin: 0;
color: #64748b;
}
.card .spacer {
/* Pushes the button to the bottom */
}
.card a {
align-self: end;
justify-self: start;
}
With this pattern, the title of every card in the same row will occupy the same vertical space, even if some titles wrap. The descriptions align, and the buttons sit at the same height. This was previously impossible without JavaScript measurement and manual positioning.
Subgrid for Form Layouts
Forms benefit enormously from subgrid. A common form pattern has labels and inputs that need to align across multiple form groups, where each group might be a component with its own markup:
.form {
display: grid;
grid-template-columns: max-content 1fr;
gap: 0.75rem 1rem;
}
.form-group {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
}
.form-group label {
grid-column: 1;
text-align: right;
padding-top: 0.5rem;
}
.form-group input,
.form-group select,
.form-group textarea {
grid-column: 2;
}
Without subgrid, each .form-group would define its own column tracks, and the labels would not necessarily align because max-content would be calculated independently for each group. With subgrid, all labels share the same first column track, which is sized to the widest label in the entire form.
When NOT to Use Grid (Flexbox Comparison)
Grid and flexbox are complementary, not competing. Here are guidelines for choosing between them:
Use flexbox when you have a one-dimensional layout (a single row or column of items), when items should size themselves based on their content, when you want items to wrap naturally without a strict column structure, or when you need to distribute space between items with justify-content and align-items. Navigation bars, button groups, icon rows, and inline tag lists are ideal flexbox use cases.
Use Grid when you need two-dimensional control (aligning items in both rows and columns), when you want a rigid column structure that items adhere to, when you need to place items in specific cells, or when you want content-independent track sizing. Page layouts, card grids, data tables, and form layouts are ideal Grid use cases.
The two combine beautifully: use Grid for the overall page structure and card grid, then use flexbox for the horizontal layout within each card's footer (e.g., aligning an icon and a link side by side).
Performance: Grid vs Flexbox for Large Layouts
For most applications, the performance difference between Grid and flexbox is negligible. Both are highly optimized in modern browsers. However, there are edge cases worth knowing about.
Flexbox with wrapping can require multiple layout passes as the browser figures out how items distribute across lines. For very large numbers of items (hundreds or thousands), this can become noticeable. Grid typically resolves layout in fewer passes because the track structure is predetermined.
Subgrid adds a dependency between parent and child layout, which means the browser must perform layout calculations in a specific order. For deeply nested subgrids (3+ levels), this could theoretically slow down layout. In practice, this is rarely a concern because subgrid nesting beyond 2 levels is uncommon.
The general advice: choose the layout method that best fits your design intent. If you have a genuine performance problem with layout, measure it with Chrome DevTools' Performance panel before switching layout methods — the bottleneck is almost always elsewhere (JavaScript, rendering, or network).
Advanced Grid Patterns
The RAM Pattern (Repeat, Auto, Minmax)
The most popular responsive grid pattern uses repeat(auto-fit, minmax(...)). It is called the RAM pattern and creates a fully responsive grid without any media queries:
.responsive-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
gap: 1.5rem;
}
The inner min(100%, 300px) prevents the minimum from being wider than the container on narrow screens, which would cause horizontal overflow. This single line of CSS handles everything from a single-column phone layout to a multi-column desktop layout.
Layering with Grid
Grid items can overlap by being placed in the same cell. Combined with z-index, this enables layering effects without absolute positioning:
.hero {
display: grid;
grid-template: 1fr / 1fr;
}
.hero > img,
.hero > .overlay {
grid-area: 1 / 1;
}
.hero > .overlay {
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
background: oklch(0% 0 0 / 40%);
color: white;
}
Browser Support
CSS Grid is supported in Chrome 57+, Firefox 52+, Safari 10.1+, and Edge 16+. It has been universally available since 2017 and can be used without any fallback concerns.
Subgrid is supported in Chrome 117+, Edge 117+, Firefox 71+, and Safari 16+. Firefox was the first browser to implement subgrid in 2019, with Safari following in 2022 and Chrome completing the set in late 2023. As of 2025, subgrid is available in all evergreen browsers.
For projects that must support older browsers without subgrid, you can use @supports (grid-template-rows: subgrid) to provide a fallback layout that uses independent nested grids with fixed row heights.
Further Reading
- Grid Generator — visually build CSS Grid layouts and export production-ready code
- All CSS Guides — explore more in-depth CSS tutorials and references
- 10 Modern CSS Features You Should Use in Every Project — container queries, nesting, and other features that pair well with Grid
- Modern CSS Color: oklch, color-mix(), and the New Color Spaces — modern color techniques for styling your grid layouts