Responsive Layouts With grid-template-areas: Rearranging Regions by Name
A page or component layout is a set of regions — header, navigation, main content, aside, footer — and responsive design is mostly about rearranging those regions as space changes. Writing that rearrangement with line numbers means updating every child's grid-column and grid-row at every breakpoint, and the numbers say nothing about intent. grid-template-areas lets you draw the layout as a picture made of region names, and each breakpoint becomes a different picture. This page shows how to build a container-driven layout from area maps, the validity rules that silently break them, and how to keep the visual order honest for keyboard and screen-reader users. It belongs to CSS Grid & Subgrid Layouts in the Mastering Container Queries & Responsive Layouts guide.
Why draw the layout
An area map is readable in a way line numbers are not. Compare grid-column: 2 / 4; grid-row: 1 / 3 with a map in which a region called main visibly occupies the middle two columns of the first two rows. The map is documentation and implementation at once: someone reading the stylesheet sees the layout's shape.
The larger benefit is where the responsive logic lives. With areas, each child is assigned to a region once — .site-nav { grid-area: nav } — and never changes. Only the container's map changes between sizes. A three-breakpoint layout is three maps on one element, rather than a dozen placement rules scattered over five children.
The complete implementation
The article layout below is driven by container width, so it adapts whether it spans the page or sits in a narrower column.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>grid-template-areas layout</title>
<style>
body { margin: 0; font: 15px/1.5 system-ui, sans-serif; }
.shell-host { container-type: inline-size; }
.shell {
display: grid;
gap: 1rem;
padding: 1rem;
/* Narrow map: one column, everything in source order. */
grid-template-areas:
"header"
"nav"
"main"
"aside"
"footer";
}
/* Children are assigned once and never change. */
.shell > header { grid-area: header; }
.shell > nav { grid-area: nav; }
.shell > main { grid-area: main; min-width: 0; }
.shell > aside { grid-area: aside; }
.shell > footer { grid-area: footer; }
/* Medium map: nav beside main. */
@container (width > 40rem) {
.shell {
grid-template-columns: 12rem 1fr;
grid-template-areas:
"header header"
"nav main"
"aside aside"
"footer footer";
}
}
/* Wide map: three columns. The dots-free map keeps every row full. */
@container (width > 64rem) {
.shell {
grid-template-columns: 12rem 1fr 16rem;
grid-template-areas:
"header header header"
"nav main aside"
"footer footer footer";
}
}
.shell > * { padding: 0.75rem; border-radius: 8px; background: #f1f5f9; }
</style>
</head>
<body>
<div class="shell-host">
<div class="shell">
<header>Header</header>
<nav aria-label="Section">Navigation</nav>
<main>Main content</main>
<aside>Related</aside>
<footer>Footer</footer>
</div>
</div>
</body>
</html>
The source order — header, nav, main, aside, footer — matches the reading order in every map: left to right, top to bottom. That is not an accident but a constraint the maps were designed around, and it is the most important rule in the accessibility section below. min-width: 0 on main stops long content from widening its column, the grid equivalent of the flexbox automatic-minimum issue in Preventing Flex and Grid Overflow.
The key technique: maps must be rectangles
grid-template-areas has strict validity rules, and violating them invalidates the entire declaration — the grid then has no named areas, and every item falls back to auto-placement, usually as one long column. The two rules:
- Every row string has the same number of cells. Cells are separated by whitespace; a row with one fewer name than its neighbours is invalid.
- Every named area is a single rectangle. A name that forms an L shape, or appears in two separate places, is invalid.
A single dot, or a run of dots such as ..., marks an empty cell. Dots keep rows the same length when a region should leave a gap, for example "header header" ". main" for a layout whose main column is offset. Aligning the strings vertically in the source, as in the example, makes errors like a short row easy to spot by eye.
Areas also create implicit named lines: an area called main produces lines named main-start and main-end in both axes. Those names are usable in line-based placement elsewhere — grid-column: main-start / aside-end — which combines the readability of areas with the flexibility of lines. The full-bleed technique in Full-Bleed Sections in Constrained Layouts relies on the same implicit naming in the other direction.
Sizing the tracks behind the map
The area map decides where regions go; grid-template-columns and grid-template-rows decide how big the tracks under them are. The two are independent, and getting the track sizes right is what keeps a layout robust when content changes.
For columns, prefer bounded flexible sizes over fixed ones. A sidebar column of 12rem is fine until a translated navigation label needs 13; minmax(10rem, 14rem) or clamp(10rem, 22%, 14rem) gives it room to adapt. The main column should nearly always be 1fr or minmax(0, 1fr), so it takes the remaining space and can shrink below its content's minimum without forcing overflow.
For rows, leave most tracks at their default auto, which sizes each row to its content. Where a region should fill remaining height — a main area in an app shell — give its row 1fr and give the grid a height, as in the shell patterns in Sticky Footer With Flexbox or Grid. Explicit row heights in pixels are rarely right; they break as soon as content grows.
When the number of columns in the map changes between sizes, the track list must change with it: a two-column map needs a two-value grid-template-columns. Declaring both properties together in each container query keeps them in step, and the grid-template shorthand can express rows, columns and areas in one declaration for compact definitions — at the cost of being harder to read for maps with many rows.
Accessibility: keep visual order and DOM order in step
Area maps make it trivially easy to move regions anywhere, including in an order different from the source. A map that puts aside above main on narrow screens, or nav on the right of main while it comes first in the DOM, looks harmless in a screenshot. For keyboard users, focus then jumps from the top of the page to the bottom and back; for screen-reader users, the content is announced in an order that contradicts what sighted colleagues see. WCAG 1.3.2 Meaningful Sequence and 2.4.3 Focus Order both apply.
Two habits prevent it. Design every map so that reading its cells left to right, top to bottom, gives the same order as the DOM. And when a design genuinely needs a different order at some size — a promotional aside that should appear before long content on phones — change the source order and adjust the maps around it, rather than letting the map contradict the source. The CSS reading-flow property, which lets reading order follow the grid, is an emerging solution but not yet something to rely on.
Variation: named areas inside components
Areas are not only for page shells. A product card with an image, title, price, rating and button benefits from the same approach, with maps chosen by the card's container.
.product-slot { container-type: inline-size; }
.product {
display: grid;
gap: 0.5rem 1rem;
grid-template-areas:
"image"
"title"
"price"
"cta";
}
@container (width > 28rem) {
.product {
grid-template-columns: 8rem 1fr auto;
grid-template-areas:
"image title price"
"image cta cta";
}
}
.product__image { grid-area: image; }
.product__title { grid-area: title; }
.product__price { grid-area: price; }
.product__cta { grid-area: cta; }
If the card animates between layouts when its container crosses the threshold, animate a transform on the children rather than the grid itself, since grid template changes cannot be interpolated between different area maps. Container-Query-Triggered Keyframe Animations shows how to play a short entrance at the moment of the switch.
Browser support
grid-template-areas is supported in Chrome 57+, Edge 16+, Firefox 52+ and Safari 10.1+, as are CSS Grid and minmax(). Container queries, used to switch maps by container width, are supported in Chrome and Edge 105+, Firefox 110+ and Safari 16+; with a viewport media query in their place the same maps work in any grid-capable browser.
FAQ
What does grid-template-areas do?
It draws a named map of the grid as strings, one string per row, one name per cell. Children placed with grid-area: name occupy the cells with that name. Changing the map rearranges the layout without touching the children's rules.
Can grid-template-areas reorder content?
It can place any child in any area, so the visual order can differ from the DOM order. That is a common accessibility problem: keyboard focus and screen readers follow the DOM, so keep the visual reading order consistent with source order in every layout.
How do I leave a cell empty in grid-template-areas?
Use one or more dots, as in "header header" ". main". A dot or a sequence of dots marks a cell with no named area, which stays empty unless an item is placed there by line numbers.
Why is my grid-template-areas declaration ignored?
The map is invalid. Every row must have the same number of cells, and each named area must form a single rectangle. An L-shaped area or a row with a missing cell invalidates the whole declaration.
Related
- CSS Grid & Subgrid Layouts — the parent guide.
- Holy Grail Layout With Grid — the classic page shell built with areas.
- Overlapping Elements With Grid Stacking — several items in one area.
- Tabs That Become an Accordion — grid placement driving a component switch.
Related articles
More pages in the same section.