Type something to search...
CSS Grid Template Areas: Visual Layouts in Code

CSS Grid Template Areas: Visual Layouts in Code

Most CSS layout code is abstract. You read grid-column: 2 / 4 or flex: 1 1 0 and have to picture the result in your head. grid-template-areas is different. It lets you write the layout as a small text diagram right in your stylesheet, where each word stands for a region of the page. The code looks like the layout it produces.

That makes it one of the most readable tools in CSS, and one of the easiest to change. Moving a sidebar from the left to the right, or stacking everything into one column on mobile, becomes a matter of rearranging a few words. In this guide we'll cover the syntax, build a full page layout, make it responsive, and look at the rules and gotchas that decide whether a template is valid.

The Basic Idea

Grid template areas work in two steps:

  1. On the grid container, grid-template-areas describes the layout as a set of quoted strings, one per row. Each word in a string is a cell, and repeating a name across cells makes that area span them.
  2. On each child, grid-area assigns it to one of the named areas.

Here's a simple example:

<div class="page">
  <header class="page__header">Header</header>
  <nav class="page__nav">Navigation</nav>
  <main class="page__main">Main content</main>
  <footer class="page__footer">Footer</footer>
</div>
.page {
  display: grid;
  grid-template-columns: 220px 1fr;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header header"
    "nav    main"
    "footer footer";
  min-height: 100vh;
}

.page__header {
  grid-area: header;
}
.page__nav {
  grid-area: nav;
}
.page__main {
  grid-area: main;
}
.page__footer {
  grid-area: footer;
}

Read the grid-template-areas value like a picture. There are three rows and two columns. The header spans both columns in the first row. The nav and main content share the second row. The footer spans the full width at the bottom.

Notice the extra spaces in "nav main". Whitespace between names doesn't matter, so you can line the columns up visually. That's not required, but it's what makes the pattern so easy to read.

The Rules That Make a Template Valid

A template is either entirely valid or entirely ignored, so it's worth knowing the rules.

Every Row Must Have the Same Number of Cells

Each string is a row, and every row must contain the same number of cells:

/* Invalid: first row has 2 cells, second has 3 */
grid-template-areas:
  "header header"
  "nav main aside";

If the counts don't match, the whole declaration is invalid and the browser drops it. Your items then fall back to auto-placement, which usually looks like a jumbled grid.

Areas Must Be Rectangles

A named area has to form a single rectangle. You can't make an L-shape or a T-shape, and you can't split one name into two separate regions:

/* Invalid: "sidebar" forms an L-shape */
grid-template-areas:
  "sidebar main"
  "sidebar sidebar";

/* Invalid: "ad" appears in two disconnected places */
grid-template-areas:
  "ad   main"
  "nav  main"
  "ad   main";

If you need a non-rectangular shape, split it into two areas and place two elements.

Use Dots for Empty Cells

To leave a cell empty, use a period. A run of periods with no space between them also counts as a single empty cell, which helps with alignment:

grid-template-areas:
  "logo  .     search"
  "nav   nav   nav"
  "main  main  aside";

/* Equivalent, using a run of dots for alignment */
grid-template-areas:
  "logo  ..... search"
  "nav   nav   nav"
  "main  main  aside";

Names Are Custom Identifiers

Inside the template strings, names can use letters, digits, hyphens, and underscores. The catch is on the other side: grid-area takes a CSS identifier, and an identifier can't start with a digit. A template containing 2col is valid, but you can't write grid-area: 2col to target it. Names are also case-sensitive, so Main and main are different areas. Stick with lowercase, hyphenated names like primary-nav to avoid surprises.

Sizing the Tracks

grid-template-areas defines the shape of the layout, but not the size of each row and column. You set those separately with grid-template-columns and grid-template-rows, and the number of tracks you define should match the template.

.dashboard {
  display: grid;
  grid-template-columns: 240px 1fr 1fr 320px;
  grid-template-rows: 64px 1fr 1fr;
  grid-template-areas:
    "sidebar topbar  topbar  topbar"
    "sidebar chart   chart   activity"
    "sidebar table   table   activity";
  gap: 1rem;
  height: 100vh;
}

Four columns in the template, four column sizes. Three rows, three row sizes. If you define fewer sizes than the template needs, the extra tracks are implicit and sized by grid-auto-columns or grid-auto-rows, which default to auto.

The grid-template Shorthand

You can combine areas and track sizes into one declaration with grid-template. Row sizes go at the end of each string, and column sizes go after a slash at the end:

.page {
  display: grid;
  grid-template:
    "header header" auto
    "nav    main" 1fr
    "footer footer" auto
    / 220px 1fr;
}

This is compact and keeps each row's height next to the row itself. Some teams love it; others find the separate properties easier to scan. Either is fine; pick one style and use it consistently.

Building a Real Layout

Let's build a blog post layout with a header, a table of contents, the article, a related-posts sidebar, and a footer.

<div class="post-layout">
  <header class="post-layout__header">
    <a href="/" class="logo">TideWave</a>
  </header>
  <nav class="post-layout__toc" aria-label="Table of contents">
    <ol>
      <li><a href="#intro">Intro</a></li>
      <li><a href="#setup">Setup</a></li>
      <li><a href="#wrap-up">Wrap-up</a></li>
    </ol>
  </nav>
  <article class="post-layout__article">
    <h1>Post title</h1>
    <p>Article content goes here.</p>
  </article>
  <aside class="post-layout__related">Related posts</aside>
  <footer class="post-layout__footer">Footer</footer>
</div>
.post-layout {
  display: grid;
  gap: 1.5rem;
  max-width: 80rem;
  margin-inline: auto;
  padding: 1rem;
  grid-template-columns: 1fr;
  grid-template-areas:
    "header"
    "article"
    "related"
    "footer";
}

.post-layout__header {
  grid-area: header;
}
.post-layout__toc {
  grid-area: toc;
}
.post-layout__article {
  grid-area: article;
}
.post-layout__related {
  grid-area: related;
}
.post-layout__footer {
  grid-area: footer;
}

/* Hide the TOC on small screens, where it isn't in the template */
.post-layout__toc {
  display: none;
}

@media (width >= 48rem) {
  .post-layout {
    grid-template-columns: 1fr 16rem;
    grid-template-areas:
      "header  header"
      "article related"
      "footer  footer";
  }
}

@media (width >= 72rem) {
  .post-layout {
    grid-template-columns: 14rem 1fr 16rem;
    grid-template-areas:
      "header header  header"
      "toc    article related"
      "footer footer  footer";
  }

  .post-layout__toc {
    display: block;
    position: sticky;
    top: 1rem;
    align-self: start;
  }
}

This is where template areas really pay off. The mobile layout is a single column. At medium widths, related posts move into a sidebar. At large widths, the table of contents appears on the left. The HTML never changes, and the per-item grid-area rules are written once. Each breakpoint only redraws the picture.

Notice that the TOC is hidden on smaller screens. That's important: if an item refers to an area name that doesn't exist in the current template, it doesn't disappear. Grid creates implicit tracks to hold it, which usually adds an unexpected extra row or column. Either hide items that aren't in a template, or make sure every area name appears in every template.

Named Lines Come for Free

When you define a named area, grid automatically creates named lines around it. An area called main gets lines named main-start and main-end in both directions. You can use them to place other items:

.post-layout__banner {
  grid-column: article-start / related-end;
  grid-row: article;
}

That places a banner across the article and related columns, aligned to whatever row the article is in, without hardcoding line numbers. The reverse also works: if you name lines hero-start and hero-end in your track definitions, grid creates an implicit area called hero you can target with grid-area: hero.

Overlapping Items in the Same Area

Nothing stops you from putting two items in the same area. They stack on top of each other, in source order, which is a clean way to layer text over an image without absolute positioning:

<section class="hero">
  <img
    class="hero__image"
    src="/images/coast.jpg"
    alt="Rocky coastline at sunrise"
  />
  <div class="hero__content">
    <h2>Explore the coast</h2>
    <a href="/tours" class="button">View tours</a>
  </div>
</section>
.hero {
  display: grid;
  grid-template-areas: "stack";
}

.hero__image,
.hero__content {
  grid-area: stack;
}

.hero__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.hero__content {
  align-self: end;
  padding: 2rem;
  color: #ffffff;
  background: linear-gradient(transparent, rgb(0 0 0 / 0.7));
}

Both children occupy the single stack cell. The content sits on top because it comes later in the source, and you can adjust that with z-index if needed. The grid sizes the cell to the larger of the two, so the hero grows if the text gets longer, which absolute positioning can't do.

Template Areas and Subgrid

With subgrid, a nested grid can inherit the tracks of its parent. Area names defined on the parent are available inside the subgrid too, so child elements deep in the tree can align to the parent's named areas:

.card-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto;
  gap: 1rem;
}

.card {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
  grid-template-areas:
    "media"
    "title"
    "meta";
}

.card__media {
  grid-area: media;
}
.card__title {
  grid-area: title;
}
.card__meta {
  grid-area: meta;
}

Here each card defines its own area names on top of the parent's rows, and because the rows are shared through subgrid, titles and meta lines line up across every card in the row even when their content lengths differ. Subgrid is supported in all current major browsers.

Accessibility: Visual Order vs. Source Order

Template areas make it very easy to move things around visually. That's also the main risk. Screen readers and keyboard focus follow the source order of the HTML, not the visual order of the grid.

If your template places the sidebar before the main content visually but the HTML puts it after, a keyboard user tabbing through the page moves through elements in a different order than they appear. That's confusing, and in some cases it fails WCAG's focus order and meaningful sequence requirements.

A good rule of thumb: use template areas for layout arrangement, like side-by-side versus stacked, but keep the HTML in the order that makes sense when read top to bottom. If you find yourself reversing the order of interactive elements visually, rethink the markup instead.

The CSS reading-flow property is designed to address this, letting the browser follow the visual grid order for focus and reading. It has shipped in Chromium-based browsers but isn't available everywhere yet, so don't depend on it for correctness.

Debugging Template Areas

Browser DevTools make template areas easy to inspect. In Chrome, Edge, and Firefox, clicking the grid badge next to a container in the Elements panel shows an overlay, and there's an option to display area names directly on the page. This is the fastest way to spot an item that landed in an implicit track because of a typo in its grid-area name.

When a layout suddenly collapses into auto-placement, check the template for:

  • Rows with different cell counts.
  • A non-rectangular area.
  • An unclosed or mismatched quote. Any one of those invalidates the whole declaration. If the template is valid but a single item is misplaced, check its grid-area value for a typo or a name that starts with a digit.

Conclusion

grid-template-areas turns layout code into something you can read at a glance. You name the regions of a page, draw how they fit together, and assign elements to them. Responsive changes become a matter of redrawing the diagram at each breakpoint, while the HTML and the per-item rules stay put.

Keep areas rectangular, keep every row the same length, and keep your HTML in a sensible reading order. Within those rules, it's one of the most pleasant layout tools CSS has to offer.

Tags :
Share :

Related Posts

A Complete Guide to CSS Container Queries

A Complete Guide to CSS Container Queries

For more than a decade, responsive design meant one thing: media queries. You asked the browser how wide the viewport was and adjusted your layout ac

Continue Reading
A Comprehensive Guide to Installing Next.js

A Comprehensive Guide to Installing Next.js

Next.js has emerged as a powerful framework for building React applications, offering features like server-side rendering, static site generation, an

Continue Reading
Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

CSS has evolved significantly, and modern tools like clamp(), min(), and max() are powerful game-changers in dynamic styling. If you’ve struggl

Continue Reading