Creating Holy Grail and Sidebar Layouts Using CSS Grid

CSS Grid replaces float-based holy grail layouts with grid-template-areas, automatic equal-height columns, and source-order independence. Build a sidebar layout that works without hacks.

Forget the negative margins. Forget the relative positioning. Forget the clearfix that added a phantom ::after element to every row you ever built. The float-based holy grail was a 2014 tour de force of CSS hacking, and it is dead. The modern CSS Grid holy grail layout replaces that fragile stack with two declarations: grid-template-areas to name the regions of the page, and grid-template-columns to size them. When you define a grid container with min-height: 100vh and give its children area names, the equal-height columns problem vanishes. Grid items in the same row stretch to the row height by default. The source-order independence problem vanishes. The area map decides placement, not the order of elements in the markup. Here is the minimal version, a sticky sidebar variant, and a responsive collapse that needs no media queries.

The Problem the Float Hack Was Solving

The float-based holy grail needed three tricks working in concert. First, you floated three columns left with percentage widths. Second, you applied negative margins to pull the sidebar columns into place, because floats only stack left-to-right. Third, you gave the main column relative positioning and nudged it with left and right offsets to avoid overlapping the floated sidebars. Then you wrapped the whole row in a clearfix, usually a ::after element with content: "" and display: table, to force the parent to contain the floats. If any one of those steps was off by a pixel, the layout broke.

The faux-column technique was equally fragile: you created a background-image that repeated vertically behind the columns, so the sidebars appeared equal height even though they were not.

CSS Grid makes both hacks unnecessary. When you set up a grid with explicit rows, the row track has a definite height, and every grid item placed in that row stretches to fill it. That is the default alignment: justify-items and align-items both default to stretch. You do not ask for equal-height columns. You get them because the grid track defines the height for every item in it.

The Minimal Holy Grail with CSS Grid

This is the entire layout. A header, a nav, a main, an aside, and a footer, arranged with grid-template-areas. The area map is a picture of the page: three columns on the first row, three on the second, one full-width row for the footer. The grid container gets min-height: 100vh so it fills the viewport even when the content is short. Each child declares its area name with the grid-area property. No floats, no clearfix, no negative margins.

.holy-grail {
  display: grid;
  min-height: 100vh;
  grid-template-columns: minmax(150px, 25%) 1fr minmax(150px, 25%);
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header header header"
    "nav main aside"
    "footer footer footer";
}

.holy-grail header { grid-area: header; }
.holy-grail nav { grid-area: nav; }
.holy-grail main { grid-area: main; }
.holy-grail aside { grid-area: aside; }
.holy-grail footer { grid-area: footer; }

The Area Map And The Track Sizes

The grid-template-areas syntax comes from the CSS Grid Layout Level 1 specification. Each string in the value is a row, and each word in the string is a column in that row. The same name appearing in multiple cells makes the element span those cells. The header spans three columns, the footer spans three, and the middle row has three distinct areas.

The grid-template-columns declaration uses minmax(150px, 25%) for the sidebars, so they never collapse below 150px, and 1fr for the main column, which takes whatever remains. The grid-template-rows declaration sets the header and footer to auto, so they size to their content, and the middle row to 1fr, so it expands to fill the remaining viewport height. That middle row is what makes the sidebars equal height: they are both placed in the same row track, and the row is as tall as the main content, which is the tallest of the three.

Sidebar Layout: A Two-Column Baseline

Before you build the three-column holy grail, master the two-column sidebar pattern. This is the workhorse layout of dashboards, documentation, and blogs. The grid container has two columns: a sidebar and a main area. The sidebar uses minmax(200px, 30%) so it keeps a readable width on smaller screens, and the main column uses 1fr to absorb the rest. The row definition is auto 1fr auto, giving you a header, a flexible middle, and a footer. Named grid lines make the intent explicit: you name the boundaries between columns, then place items with line names instead of numbers.

.sidebar-layout {
  display: grid;
  min-height: 100vh;
  grid-template-columns: [sidebar-start] minmax(200px, 30%) [sidebar-end main-start] minmax(0, 1fr) [main-end];
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header header"
    "sidebar main"
    "footer footer";
}

.sidebar-layout header { grid-area: header; }
.sidebar-layout nav { grid-area: sidebar; }
.sidebar-layout main { grid-area: main; }
.sidebar-layout footer { grid-area: footer; }

Why minmax(0, 1fr) On The Main Column

The minmax(0, 1fr) on the main column is deliberate. Without the 0 lower bound, a long unbreakable word or a wide image inside the main area forces the fr unit to expand beyond its share, overflowing the grid. The minmax(0, 1fr) tells the browser the main column can shrink to zero, so the fr unit resolves against the available space, not against the content’s intrinsic width. This is the difference between a layout that breaks and one that scrolls horizontally inside the column instead of blowing out the page.

Sticky Sidebar: Keeping Navigation In View

A sidebar that scrolls away is a sidebar you lose. The sticky sidebar technique pins the navigation to the viewport while the main content scrolls. The property is position: sticky, and it works inside a grid track without any special grid syntax. Set the sidebar’s align-self to start so it does not stretch to the full row height, then apply position: sticky with a top value. The sticky element sticks within its containing block, which is the grid area it occupies. As long as the grid area extends below the viewport, the sidebar stays visible.

.sidebar-layout nav {
  grid-area: sidebar;
  align-self: start;
  position: sticky;
  top: 1rem;
}

The sticky positioning comes from the CSS Positioned Layout Module Level 3 specification. The containing block for a sticky element is its nearest scrolling ancestor, but within a grid, the element’s containing block is its grid area. So the sidebar sticks to the top of the viewport when you scroll, but it stops sticking when the bottom of its grid area reaches the top of the viewport. You get a navigation column that stays accessible through a long article, then scrolls away naturally when you reach the footer. The align-self: start is what prevents the sticky behaviour from failing: if the sidebar stretched to the full row height, there would be no room for it to move, and the sticky would never engage.

Three-Column Layout: The Holy Grail With Named Lines

Named grid lines give you an alternative to grid-template-areas. Instead of describing the page as a map, you name the boundaries between columns and rows, then place items using those names. Use this when you want to control the exact start and end of each item, especially when an item spans more than one track. The holy grail with named lines looks like this.

.holy-grail-named {
  display: grid;
  min-height: 100vh;
  grid-template-columns:
    [left-start] minmax(150px, 25%) [left-end center-start] minmax(0, 1fr) [center-end right-start] minmax(150px, 25%) [right-end];
  grid-template-rows: [header-start] auto [header-end content-start] 1fr [content-end footer-start] auto [footer-end];
}

.holy-grail-named header {
  grid-column: left-start / right-end;
  grid-row: header-start / header-end;
}

.holy-grail-named nav {
  grid-column: left-start / left-end;
  grid-row: content-start / content-end;
}

.holy-grail-named main {
  grid-column: center-start / center-end;
  grid-row: content-start / content-end;
}

.holy-grail-named aside {
  grid-column: right-start / right-end;
  grid-row: content-start / content-end;
}

.holy-grail-named footer {
  grid-column: left-start / right-end;
  grid-row: footer-start / footer-end;
}

When Named Lines Beat The Area Map

The named lines version is more verbose, but it makes the source order dependence explicit. The header and footer span the full width because they start at the left edge and end at the right edge. The three middle items all occupy the content row, which is the 1fr row that stretches to fill the viewport. Because they share the same row track, they are equal height by default. The grid-template-columns declaration with named lines also lets you reference the boundaries in media queries or in other grid items, useful when you need to align items across unrelated parts of the page.

Holy Grail Flexbox vs Grid: What Each Tool Does Well

The holy grail flexbox vs grid question comes up every time someone learns the newer tool. Flexbox is one-dimensional. It distributes items along a single axis, either the main axis or the cross axis. You can force a three-column layout with flexbox by setting flex: 1 1 0% on the main content and explicit widths on the sidebars, but you cannot make the columns equal height without a wrapper that stretches them. You cannot reorder items without the order property, which changes visual order but not source order for assistive technology.

Grid is two-dimensional. It aligns items across both rows and columns simultaneously. The area map defines the visual order, and the source order can be anything because the grid-area property places each item. For a layout that needs alignment in both axes, grid is the correct choice. Flexbox remains the better tool for a single row of buttons, a toolbar, or a card’s internal layout where you only need to distribute items along one axis. The distinction is not macro vs micro. It is one-axis vs two-axis.

Responsive Collapse Without Media Queries: auto-fit and minmax

The grid holy grail collapses to a single column on narrow screens, and you can do it without a single media query. The technique uses auto-fit in the grid-template-columns value with a minmax that sets the minimum column width. When the container is wider than that minimum, auto-fit creates as many columns as fit. When the container is narrower, the columns wrap into rows. For the holy grail, you want the three columns to stack vertically when the viewport narrows. The area map handles this: you redefine the areas to be single-column rows, and the grid-template-columns to be 1fr.

.holy-grail {
  display: grid;
  min-height: 100vh;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  grid-template-areas:
    "header"
    "nav"
    "main"
    "aside"
    "footer";
}

In this version, the grid has a single column that is at least 16rem wide and expands to fill the container. The auto-fit keyword tells the grid to create as many tracks as fit, but because the area map only defines one column, the items stack vertically. The header, nav, main, aside, and footer each get their own row. The minmax(16rem, 1fr) ensures the column never shrinks below 16rem on a phone, so text stays readable. This replaces the @media (max-width: 600px) fallback that the CSS Grid Layout Module Level 1 specification suggests in its section on Reordering and Accessibility. That specification section notes that reordering content with grid can change the reading order, so you must ensure the source order matches the logical order when the layout collapses. Here, the source order is header, nav, main, aside, footer, which is the same as the stacked order, so there is no accessibility problem.

Common Mistakes: What Breaks The Grid

Two mistakes destroy more grid layouts than anything else.

First, forgetting min-height: 100vh on the grid container. Without it, the grid shrinks to the content height, and the 1fr row collapses to zero. The sidebars are then only as tall as their content. That defeats the equal-height purpose.

Second, using grid-template-areas without assigning a placement property to every child. If a child lacks a grid-area name, the grid auto-places it into the first available cell, which is the header region, overlapping the named items. The result is a jumbled mess that looks like the grid is broken when the real problem is a missing property.

A third mistake, specific to sidebars, is using a fixed pixel width in grid-template-columns. A sidebar of width: 250px is fine on a desktop, but it eats the whole screen on a phone. Use minmax(200px, 30%) or minmax(200px, 1fr) so the sidebar can shrink or grow with the viewport. The fr unit and minmax are the tools for responsive sizing, not pixels.

FAQ

Does the CSS Grid holy grail layout work in all browsers?

All modern browsers support CSS Grid Layout Level 1, including the grid-template-areas and minmax() syntax. The only exception is Opera Mini, which does not support grid. If you must support Opera Mini, provide a fallback with flexbox or block layout. Check caniuse for the current support picture.

Can I use position: sticky with grid?

Yes. position: sticky works inside a grid item. Set align-self: start on the sticky item so it does not stretch to the full row height, then give it a top value. The sticky element will stick within its grid area.

What is the difference between auto-fit and auto-fill?

auto-fit collapses empty tracks to zero width, so the existing tracks expand to fill the container. auto-fill keeps empty tracks at their minimum size, leaving gaps. For a single-column collapse, use auto-fit.

Does grid-template-areas reorder content for screen readers?

No. Visual order changes with grid-template-areas, but the accessibility tree follows source order. The CSS Grid Level 1 spec warns about this in Section 8.3. Keep source order matching the stacked order for responsive layouts.

The One Thing That Makes This Page Different

The float-based holy grail with negative margins and clearfix is not just old. It is a solved problem, and the solution is two lines of CSS. The specific insight here is this: the minmax(0, 1fr) on the main column is not a minor detail. It is the difference between a grid that respects the viewport width and a grid that overflows horizontally when an image or a long URL appears. Most tutorials omit it, and their examples break in production. This page does not.