Mastering CSS Grid: Grid Areas, Item Alignment, and Spanning
Introduction
Welcome to Part 2 of the CSS Grid Fundamentals Series!
In Part 1, you laid the groundwork — learning how to define grid containers, set up tracks, and build basic layouts.
Now, it's time to go beyond the basics.
Think of CSS Grid as your layout canvas — and in this part, you'll pick up new tools like a ruler, compass, and highlighter to draw structure, alignment, and flow with precision.
We'll cover:
grid-template-areasand named grid lines for clean, visual layout structure- Alignment techniques using
place-itemsandplace-self - Distributing tracks with
justify-contentandalign-contentwhen your grid is smaller than its container - Spanning elements across rows and columns — and keeping visual order accessible
- The hidden mechanics of explicit and implicit grids
subgrid, for keeping nested grids aligned to their parent's tracks
These are the techniques that elevate your layouts from functional to flexible and scalable — especially in real-world, component-based frontend projects.
Grid Template Areas
The grid-template-areas property allows you to define your layout using named areas, making the grid structure more readable and easier to maintain — especially for complex page layouts. It acts as a visual blueprint of your design and simplifies the placement of grid items.
.grid {
display: grid;
grid-template-areas:
"header header header"
"sidebar main main"
"footer footer footer";
grid-template-columns: 1fr 2fr 2fr;
grid-template-rows: auto 1fr auto;
padding: 1rem;
gap: 1rem;
}Each quoted line defines a row in the grid. The repeated area names indicate how many columns that area spans. You can then assign elements to these areas using the grid-area property:
.header {
grid-area: header;
background: #f8b400;
padding: 1rem;
}
.main {
grid-area: main;
background: #4caf50;
padding: 1rem;
}
.sidebar {
grid-area: sidebar;
background: #2196f3;
padding: 1rem;
}
.footer {
grid-area: footer;
background: #9c27b0;
padding: 1rem;
}Here's the HTML structure:
<div class="grid">
<header class="header">Header</header>
<aside class="sidebar">Sidebar</aside>
<main class="main">Main Content</main>
<footer class="footer">Footer</footer>
</div>Explanation:
- The layout is visually defined using
grid-template-areas, making it easier to understand and modify. - The grid has 3 columns and 3 rows.
- Named areas like
"header","sidebar","main", and"footer"are assigned usinggrid-area, allowing semantic HTML elements to be positioned without relying on source order. - The column layout uses
1fr 2fr 2fr, giving the sidebar less space than the main content. - This is a clean, semantic approach to building responsive page sections — no floats or nested containers needed.
👉 Try this hands-on in the Template Areas tab of the interactive playground below.
👉 Want to see this exact technique applied to a full page? The Holy Grail Layout snippet builds the classic header/nav/main/aside/footer layout with grid-template-areas — including a responsive breakpoint that re-flows it into a single column.
Naming Grid Lines Directly
grid-template-areas names whole regions, but you can also name the individual lines that make up your columns and rows — useful when you want semantic placement without carving out full named areas.
.grid {
display: grid;
grid-template-columns:
[sidebar-start] 250px
[sidebar-end main-start] 1fr
[main-end];
gap: 1rem;
}
.sidebar {
grid-column: sidebar-start / sidebar-end;
}
.main {
grid-column: main-start / main-end;
}Each name in square brackets marks a line, and a single line can carry multiple names (like sidebar-end main-start above, marking where one track ends and the next begins). Items then reference those names in grid-column / grid-row instead of raw numbers.
Visual Illustration
Aligning Grid Items
CSS Grid not only lets you organize content into rows and columns — it also gives you precise control over how items are aligned within their grid cells.
Alignment can be applied at two levels:
Container-Level Alignment
These properties are applied to the grid container and affect all items inside the grid:
- justify-items — Aligns items horizontally (inline axis) within each cell.
- align-items — Aligns items vertically (block axis) within each cell.
- place-items — A shorthand for combining both
align-itemsandjustify-items, written in that order:place-items: <align-items> <justify-items>.
.grid {
display: grid;
justify-items: center; /* Horizontally center all grid items within their cells */
align-items: center; /* Vertically center all grid items within their cells */
}Visual Reference (Centered Items)
You can also use place-items as a shorthand — remember, the first value is align-items and the second is justify-items, so end start means align-items: end (bottom) then justify-items: start (left):
.grid {
display: grid;
place-items: end start; /* align-items: end (bottom), justify-items: start (left) */
}Visual Reference (Bottom-Left Aligned)
Item-Level Alignment
While container-level alignment applies to all grid items, item-level alignment lets you override alignment per individual item. These properties are set directly on the grid items themselves:
- justify-self — Aligns an item horizontally (inline axis) within its grid cell.
- align-self — Aligns an item vertically (block axis) within its grid cell.
- place-self — A shorthand for combining
align-selfandjustify-self, written in that order:place-self: <align-self> <justify-self>.
.item {
justify-self: end; /* Aligns this item to the right (inline-end) of its cell */
align-self: start; /* Aligns this item to the top (block-start) of its cell */
}Visual Reference (Top-Right Alignment)
You can also use place-self as a shorthand — remember, the first value is align-self and the second is justify-self, so end start means align-self: end (bottom) then justify-self: start (left):
.item {
place-self: end start; /* align-self: end (bottom), justify-self: start (left) */
}Visual Reference (Bottom-Left Alignment)
👉 Try this hands-on in the Container Alignment and Self-Alignment panels of the Alignment & Spanning tab in the interactive playground below.
Distributing Grid Tracks
Everything in the previous section aligned an item within its own cell. justify-content and align-content solve a
different problem: what happens to the extra space when the grid's tracks, added together, don't fill the container?
That only happens when tracks have a fixed size (like 100px) instead of a flexible one (like 1fr, which always grows
to consume every available pixel) — so this is the first place in this post where sizing your tracks in fixed units
instead of fr actually matters.
- justify-content — distributes the grid's columns horizontally within the container's leftover width.
- align-content — distributes the grid's rows vertically within the container's leftover height.
- place-content — a shorthand for combining both, written
place-content: <align-content> <justify-content>.
.grid {
display: grid;
grid-template-columns: repeat(3, 80px); /* Fixed tracks — 240px total, container is wider */
justify-content: space-between; /* Distribute the extra horizontal space between the columns */
}Three values cause most of the confusion in practice — space-between, space-around, and space-evenly — and the
difference between them is entirely about what happens at the edges:
space-between— no space before the first track or after the last; all the extra space goes strictly between tracks.space-around— every track gets equal space on both sides, so the edge gaps end up half the size of the gaps between tracks.space-evenly— every gap, edge or between, is exactly equal.
Visual Illustration
👉 Try this hands-on in the new Content Distribution tab of the interactive playground below.
Spanning Grid Items
CSS Grid allows items to span across multiple columns or rows using grid-column and grid-row. These properties let you define where an item starts and ends in the grid layout.
Imagine a 3×3 grid:
<div class="grid">
<div class="item">Spanning</div>
<div>Item 2</div>
<div>Item 3</div>
<div>Item 4</div>
</div>.grid {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 3 equal columns */
grid-template-rows: repeat(3, 100px); /* 3 equal rows */
gap: 0.5rem;
}And you apply:
.item {
grid-column: 1 / 3; /* Spans from column line 1 to 3 → covers columns 1 and 2 */
grid-row: 2 / 4; /* Spans from row line 2 to 4 → covers rows 2 and 3 */
}Visual Reference (Lines Shown)
Shorthand vs Longhand
The grid-column and grid-row properties are shorthands for their respective start and end lines:
.item {
grid-column: 2 / 4;
grid-row: 1 / 3;
}This is functionally the same as:
.item {
grid-column-start: 2;
grid-column-end: 4;
grid-row-start: 1;
grid-row-end: 3;
}Shorthand with span
Instead of specifying start and end lines manually, you can use the span keyword to make your code simpler and more flexible:
.item {
grid-column: span 2; /* Span 2 columns from auto-start */
grid-row: span 2; /* Span 2 rows from auto-start */
}This tells the grid item to span 2 columns and 2 rows starting from its auto-determined position (i.e. where the browser places it in the flow).
👉 Try this hands-on in the Spanning (Hero Item) panel of the Alignment & Spanning tab in the interactive playground below.
Accessibility: Visual Order vs. DOM Order
grid-column and grid-row let you place an item anywhere in the grid — but placement only changes where it appears on screen. It doesn't move the element in the HTML.
That mismatch matters for two groups of users:
- Keyboard users tab through elements in DOM order, not visual order. If a "Submit" button is moved visually to appear first but is still last in the markup, keyboard focus will jump around in a way that doesn't match what's on screen.
- Screen reader users hear content read in DOM order by default, so a visually-reordered layout can be announced in a confusing sequence.
Visual Illustration
If you do need a layout where visual and reading order diverge — for example, a sidebar that appears first on screen but should be read last — test it with keyboard-only navigation (Tab) and a screen reader to confirm the experience still makes sense.
Grid Types
In CSS Grid, it's important to understand the two types of grids: explicit and implicit. These define how grid tracks (rows and columns) are created — either by the developer or automatically by the browser.
Explicit Grids
An explicit grid is one that you define yourself using properties like grid-template-columns and grid-template-rows.
.grid {
display: grid;
grid-template-columns: 1fr 1fr; /* Two equal-width columns */
grid-template-rows: 100px 100px; /* Two fixed-height rows */
gap: 1rem;
}<div class="grid">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
<div>Item 4</div>
</div>In this case, you've explicitly created a 2×2 grid, which gives you 4 cells. Each of the 4 items is placed within this defined layout.
Visual Illustration
If you place more items than your explicit tracks can hold, the grid will automatically generate new rows or columns to accommodate them — these are known as implicit tracks, which we'll explore next.
Implicit Grids
In CSS Grid, if the number of items exceeds the explicitly defined rows or columns, the browser automatically creates additional tracks — these form what's known as the implicit grid.
You can control the size of these extra rows and columns using:
- grid-auto-rows – defines height of implicit rows.
- grid-auto-columns – defines width of implicit columns.
Example: Implicit Rows
Let's say you define only 2 rows, but add 5 items:
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 100px 100px; /* Explicit: only 2 rows defined */
gap: 1rem;
}<div class="grid">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
<div>Item 4</div>
<div>Item 5</div> <!-- This item doesn't fit in the defined grid -->
</div>What happens?
- The first 4 items fill the 2×2 grid.
- The 5th item overflows the defined rows.
- So, the browser creates a 3rd row automatically — this is the implicit grid.
Visual Illustration
By default, the height of implicit rows is auto. You can control their size using grid-auto-rows:
.grid {
grid-auto-rows: 100px; /* Applies height to all implicit rows */
}👉 Try this hands-on in the Explicit vs Implicit Grid panel of the Alignment & Spanning tab — push the item count past 9 to see the browser add a 3rd row, in the interactive playground below.
Example: Implicit Columns
Now let's say you define only 2 columns, but use grid-auto-flow: column so overflow items push into new columns instead of new rows:
.grid {
display: grid;
grid-template-columns: 150px 150px; /* Explicit: only 2 columns defined */
grid-template-rows: 100px 100px;
grid-auto-flow: column; /* Place items column-wise */
grid-auto-columns: 150px; /* Width for any implicitly created columns */
}<div class="grid">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
<div>Item 4</div>
<div>Item 5</div> <!-- This item doesn't fit in the defined grid -->
</div>What happens?
- With
grid-auto-flow: column, items fill column by column: Item 1 and Item 2 fill column 1, Item 3 and Item 4 fill column 2 — using up both explicit columns. - Item 5 has nowhere left to go, so the browser generates a 3rd column implicitly rather than wrapping to a new row.
- That new column is 150px wide (because of
grid-auto-columns), and since only Item 5 lands in it, the column's second cell stays empty.
Visual Illustration
Use grid-auto-columns to control the width of these new columns.
👉 In that same panel, switch grid-auto-flow to column to see implicit columns generated instead, in the interactive playground below.
Subgrid: Aligning Nested Grids
So far, every grid you've built has been self-contained — a grid item is a black box to its own children. If that item has its own display: grid children, they size themselves independently, with no awareness of the parent's tracks.
subgrid changes that. It lets a nested grid adopt its parent's column or row tracks instead of defining its own — so content inside nested grid items can line up across siblings, even when those siblings have different content lengths.
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.card {
display: grid;
grid-template-rows: subgrid; /* Inherit the parent's row tracks */
grid-row: span 3; /* Occupy 3 of the parent's rows */
}<div class="cards">
<div class="card">
<h3>Title</h3>
<p>A short description.</p>
<button>Buy now</button>
</div>
<div class="card">
<h3>A much longer title that wraps</h3>
<p>A longer description with more detail spanning multiple lines.</p>
<button>Buy now</button>
</div>
</div>Why this matters: without subgrid, each .card sizes its own three rows (title, description, button) independently — so if one card's title wraps to two lines, that card's button drops out of alignment with the others. With grid-template-rows: subgrid, every card's title, description, and button rows are the parent's tracks, so they stay aligned across every card regardless of content length.
Visual Illustration
Common Mistakes with Alignment & Spanning
- Confusing
justify-items/align-itemswithjustify-content/align-content. The-itemsproperties align content inside each cell; the-contentproperties (see Distributing Grid Tracks above) align the entire track grid within the container when the grid is smaller than its container. Mixing them up is one of the most common Grid debugging rabbit holes. - Off-by-one errors with line numbers. Grid lines are numbered starting at
1, not0— and a grid with3columns has4lines, not3.grid-column: 1 / 3spans two tracks (columns 1 and 2), not three. - Forgetting you can count from the end. Line
-1always refers to the last line in the explicit grid, no matter how many tracks it has —grid-column: 1 / -1is a quick way to span an item across every column without knowing the exact count. - Leaving
grid-auto-rows/grid-auto-columnsunitless. Unlikefron explicit tracks, implicit tracks need an explicit unit (grid-auto-rows: 100px, notgrid-auto-rows: 1) — a bare number is invalid and the browser falls back toauto.
Interactive Example
Alignment, spanning, and implicit tracks are easiest to internalize together. Use the Alignment & Spanning tab below to set
container-level justify-items / align-items for every cell (or flip the checkbox to see the same rule written as the
place-items / place-self shorthand), then self-align and span just the highlighted hero item, each in its own panel. The last
panel raises the item count past the explicit 3×3 grid so you can watch the browser generate implicit tracks — via grid-auto-flow,
grid-auto-rows, and grid-auto-columns — in real time.
Switch to the Content Distribution tab to set justify-content / align-content (or the place-content shorthand) on a grid
whose fixed-size tracks are deliberately smaller than their container, and watch space-between, space-around, and space-evenly
redraw the gaps around them live.
Switch to the Template Areas tab to revisit named regions from the top of this post — swap between a few preset layouts and
watch the same colored blocks reflow into a completely different shape, with the generated grid-template-areas and grid-area
rules shown live below the preview.
Grid Alignment, Spanning & Template Areas Playground
Set container-level justify-items/align-items (or the place-items/place-self shorthand), span and self-align a hero item, raise the item count past the explicit grid to watch implicit tracks appear, distribute fixed-size tracks with justify-content/align-content, and switch to the Template Areas tab to swap between named-region layout presets.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>CSS Grid Playground</title> </head> <body> <main class="playground"> <header class="playground-header"> <h1>CSS Grid Playground</h1> <p>Switch between alignment/spanning and named template areas — everything below updates live.</p> </header> <div class="tabs" role="tablist" aria-label="Demo mode"> <button class="tab tab--active" type="button" data-mode="alignment" role="tab" aria-selected="true">Alignment & Spanning</button> <button class="tab" type="button" data-mode="distribution" role="tab" aria-selected="false">Content Distribution</button> <button class="tab" type="button" data-mode="areas" role="tab" aria-selected="false">Template Areas</button> </div> <div id="alignment-demo" class="tab-panel tab-panel--active"> <section class="demo-area" aria-label="Grid preview"> <div id="grid-preview" class="grid-preview"></div> </section> <output id="grid-output" class="readout" aria-live="polite"></output> <fieldset class="control-group"> <legend>Container Alignment (all items)</legend> <div class="fields-grid"> <div class="field"> <label for="justify-items-select">justify-items</label> <select id="justify-items-select"> <option value="stretch">stretch</option> <option value="start">start</option> <option value="center" selected>center</option> <option value="end">end</option> </select> </div> <div class="field"> <label for="align-items-select">align-items</label> <select id="align-items-select"> <option value="stretch">stretch</option> <option value="start">start</option> <option value="center" selected>center</option> <option value="end">end</option> </select> </div> <div class="field"> <label for="place-items-select">place-items <span class="shorthand-note">(shorthand)</span></label> <select id="place-items-select"></select> </div> </div> </fieldset> <fieldset class="control-group"> <legend>Self-Alignment (Hero Item)</legend> <div class="fields-grid"> <div class="field"> <label for="justify-self-select">justify-self</label> <select id="justify-self-select"> <option value="">(inherit)</option> <option value="stretch">stretch</option> <option value="start">start</option> <option value="center">center</option> <option value="end">end</option> </select> </div> <div class="field"> <label for="align-self-select">align-self</label> <select id="align-self-select"> <option value="">(inherit)</option> <option value="stretch">stretch</option> <option value="start">start</option> <option value="center">center</option> <option value="end">end</option> </select> </div> <div class="field"> <label for="place-self-select">place-self <span class="shorthand-note">(shorthand)</span></label> <select id="place-self-select"></select> </div> </div> </fieldset> <fieldset class="control-group"> <legend>Spanning (Hero Item)</legend> <div class="fields-grid"> <div class="field"> <label for="column-span-input">grid-column: span</label> <div class="range-row"> <span class="range-edge">1</span> <div class="range-track"> <input type="range" id="column-span-input" min="1" max="3" step="1" value="1"> <output class="range-bubble" id="column-span-value" for="column-span-input">1</output> </div> <span class="range-edge">3</span> </div> </div> <div class="field"> <label for="row-span-input">grid-row: span</label> <div class="range-row"> <span class="range-edge">1</span> <div class="range-track"> <input type="range" id="row-span-input" min="1" max="3" step="1" value="1"> <output class="range-bubble" id="row-span-value" for="row-span-input">1</output> </div> <span class="range-edge">3</span> </div> </div> </div> </fieldset> <fieldset class="control-group"> <legend>Explicit vs Implicit Grid</legend> <div class="fields-grid"> <div class="field"> <label for="item-count-input">Item count</label> <div class="range-row"> <span class="range-edge">9</span> <div class="range-track"> <input type="range" id="item-count-input" min="9" max="16" step="1" value="9"> <output class="range-bubble" id="item-count-value" for="item-count-input">9</output> </div> <span class="range-edge">16</span> </div> </div> <div class="field"> <label for="auto-flow-select">grid-auto-flow</label> <select id="auto-flow-select"> <option value="row" selected>row</option> <option value="column">column</option> </select> </div> <div class="field"> <label for="auto-track-input"><span id="auto-track-label">grid-auto-rows</span></label> <div class="range-row"> <span class="range-edge">40px</span> <div class="range-track"> <input type="range" id="auto-track-input" min="40" max="140" step="10" value="60"> <output class="range-bubble" id="auto-track-value" for="auto-track-input">60px</output> </div> <span class="range-edge">140px</span> </div> </div> </div> <p class="tip">The first 9 items (<span class="legend-swatch explicit-swatch"></span> solid) fill the explicit 3×3 grid. Raise the item count and the rest (<span class="legend-swatch implicit-swatch"></span> dashed) overflow into browser-generated implicit tracks.</p> </fieldset> </div> <div id="distribution-demo" class="tab-panel"> <section class="demo-area" aria-label="Content distribution preview"> <div id="distribution-preview" class="distribution-preview"> <div class="distribution-track"></div> <div class="distribution-track"></div> <div class="distribution-track"></div> <div class="distribution-track"></div> <div class="distribution-track"></div> <div class="distribution-track"></div> </div> </section> <output id="distribution-output" class="readout" aria-live="polite"></output> <fieldset class="control-group"> <legend>Distribute Tracks</legend> <div class="fields-grid"> <div class="field"> <label for="justify-content-select">justify-content</label> <select id="justify-content-select"> <option value="start">start</option> <option value="center">center</option> <option value="end">end</option> <option value="stretch">stretch</option> <option value="space-between" selected>space-between</option> <option value="space-around">space-around</option> <option value="space-evenly">space-evenly</option> </select> </div> <div class="field"> <label for="align-content-select">align-content</label> <select id="align-content-select"> <option value="start" selected>start</option> <option value="center">center</option> <option value="end">end</option> <option value="stretch">stretch</option> <option value="space-between">space-between</option> <option value="space-around">space-around</option> <option value="space-evenly">space-evenly</option> </select> </div> <div class="field"> <label for="place-content-select">place-content <span class="shorthand-note">(shorthand)</span></label> <select id="place-content-select"></select> </div> </div> <p class="tip">These 6 tracks are fixed at 70×50px each inside a wider, taller container — on purpose, so there's leftover space for <code>justify-content</code>/<code>align-content</code> to distribute. Switch either to <code>stretch</code> to see it do nothing here: a fixed-size track never grows past its own size.</p> </fieldset> </div> <div id="areas-demo" class="tab-panel"> <section class="demo-area" aria-label="Template areas preview"> <div id="areas-preview" class="areas-preview"></div> </section> <output id="areas-output" class="readout" aria-live="polite"></output> <fieldset class="control-group"> <legend>Layout Preset</legend> <div class="fields-grid"> <div class="field"> <label for="areas-preset-select">grid-template-areas</label> <select id="areas-preset-select"> <option value="holy-grail" selected>Holy Grail</option> <option value="dashboard">Dashboard</option> <option value="sidebar-right">Sidebar Right</option> </select> </div> <div class="field"> <label for="areas-gap-input">Gap</label> <div class="range-row"> <span class="range-edge">0px</span> <div class="range-track"> <input type="range" id="areas-gap-input" min="0" max="24" step="2" value="8"> <output class="range-bubble" id="areas-gap-value" for="areas-gap-input">8px</output> </div> <span class="range-edge">24px</span> </div> </div> </div> <p class="tip">Every colored block is a real element assigned to a named region with <code>grid-area</code> — swap the preset to see the same markup reflow into a completely different shape, with zero source-order changes.</p> </fieldset> </div> </main> <script src="./index.js"></script> </body> </html>
Starting sandbox…
No console output yet.
No original version of /index.html to compare against.
Conclusion
You've now mastered the core tools that bring power, readability, and precision to CSS Grid layouts:
- Named areas — and named lines — help you design layouts visually and semantically.
- Item alignment ensures pixel-perfect placement in every cell.
justify-content/align-contentdistribute the whole track grid within its container, once you know that's a different job from aligning items.- Spanning techniques let you control flow and sizing beyond automatic placement — with an eye on keeping visual and DOM order in sync for accessibility.
- Explicit vs. implicit grids give you confidence when your layout grows unexpectedly.
- Subgrid lets nested grids inherit their parent's tracks, so content stays aligned across siblings.
Up next in Part 3 → Responsive CSS Grid Layouts: fr Units, minmax(), auto-fill, and auto-fit Explained, we'll unlock how to:
- Use
fr,minmax(),auto-fit, andauto-fillfor fluid responsive tracks - Pair grid with media and container queries
- Build robust, flexible layouts that adapt to any screen — no media query soup needed!
A lot of testing and refining went into getting these grid-area, alignment, spanning, subgrid, and accessibility examples just right, and I hope that effort shows in how clearly they click for you. Thanks for following along — hope your layouts feel a little more precise now. 📐