Build the holy grail layout — the classic page shape with a full-width header on top, a full-width footer on the bottom, and three columns in between: a navigation sidebar, a flexible main content area, and a secondary sidebar. It earned its name because, for years, doing it well (fluid centre, fixed sidebars, equal heights, sensible source order) was genuinely hard with the tools of the time. With CSS Grid it's now a few lines — which is exactly what this exercise teaches.
// A self-contained, presentational component. No props, no state.
function App(): JSX.Element;
Render five regions — header, nav, main, aside, footer — arranged in the holy-grail shape.
┌──────────────── header ────────────────┐
│ nav │ main │ aside │
└──────────────── footer ────────────────┘
- header & footer span all three columns
- nav & aside are fixed width; main fills the remaining space
- all three middle columns share the same height
grid-template-areas lets you name the five regions and "draw" the layout in CSS — the centre column flexes (1fr) while the sidebars stay fixed.You'll build the page layout that was famously fiddly for a decade — and watch CSS Grid's named areas collapse it into a handful of declarative lines.
Almost every content site has the same skeleton: a banner across the top, a footer across the bottom, and a three-column band in the middle — a nav rail, the main content, and a secondary sidebar. The hard parts historically were making the centre column fluid while the rails stayed fixed, keeping all three columns the same height, and not having to put the sidebars before the content in the HTML. Grid solves all three at once: you name the regions and place them on a template.
Picture a 3×3 grid. The top row is one wide cell (the header), the bottom row is one wide cell (the footer), and the middle row splits into three cells (nav, main, aside). With grid-template-areas you literally draw that map as strings, then tell each region which named area it belongs to. The columns are sized 150px 1fr 150px — fixed rails, fluid centre — and because grid cells in a row stretch to the tallest, the columns are equal height automatically.
Before Grid, the go-to was floating the three middle columns:
<div className="middle">
<nav style={{ float: 'left', width: 150 }}>…</nav>
<aside style={{ float: 'right', width: 150 }}>…</aside>
<div className="main">…</div> {/* fills the middle */}
</div>
This works only with care and quickly bites back. Floated columns don't share a height, so the sidebars stop short of the content (or vice-versa) and their backgrounds don't reach the footer. You need a clearfix so the footer doesn't ride up into the floats, and to avoid the centre slipping under a sidebar you often have to reorder the HTML — sidebars before the content — which hurts reading order and accessibility. Floats were never meant for page layout.
import './styles.css';
export default function App() {
return (
<main className="container">
<h1>Holy Grail</h1>
{/* One grid; each child is placed onto a named area. */}
<div className="holy-grail">
<header className="hg-header">Header</header>
<nav className="hg-nav">
<h2>Nav</h2>
<ul><li>Home</li><li>About</li><li>Contact</li></ul>
</nav>
<div className="hg-main">
Main content. This column flexes to fill the space between the two
fixed sidebars, and all three share the same height.
</div>
<aside className="hg-aside">
<h2>Aside</h2>
<ul><li>Links</li><li>Ads</li><li>Widgets</li></ul>
</aside>
<footer className="hg-footer">Footer</footer>
</div>
</main>
);
}
.holy-grail {
display: grid;
grid-template-areas:
'header header header'
'nav main aside'
'footer footer footer';
grid-template-columns: 150px 1fr 150px; /* fixed | fluid | fixed */
grid-template-rows: auto minmax(220px, 1fr) auto;
gap: 12px;
}
.hg-header { grid-area: header; }
.hg-nav { grid-area: nav; }
.hg-main { grid-area: main; }
.hg-aside { grid-area: aside; }
.hg-footer { grid-area: footer; }
The shift from floats: instead of nudging boxes left and right and cleaning up afterwards, you declare the whole map once. grid-template-areas names the five regions; grid-template-columns: 150px 1fr 150px makes the rails fixed and the centre fluid; equal heights and the spanning header/footer fall out of the grid model for free. The HTML can stay in natural reading order — header, nav, main, aside, footer — regardless of where each lands visually.
There's no runtime logic — the browser resolves the layout from the grid declarations. Say the container is 900px wide with gap: 12px:
150px rails take 300px; two gaps take 24px; the 1fr main column absorbs the remaining 900 − 300 − 24 = 576px.auto (as tall as its content), the footer row is auto, and the middle row is minmax(220px, 1fr) — at least 220px, growing if there's space.grid-area matches a name in the template, so <header> fills the full top row, nav/main/aside fill the middle three cells, and <footer> fills the full bottom row — no matter their order in the HTML.Widen or narrow the container and only the centre column changes width — the rails stay put and the columns stay aligned.
1fr so it absorbs whatever's left.grid-template-areas and a grid-area silently drops the region into an implicit track. Fix: keep the names identical.grid-template-areas rows. Every row string must list the same number of columns, or the template is invalid and ignored. Fix: keep all rows the same column count.grid-template-areas in a media query to stack all five regions into one column.Grid describes the whole page at once; this alternative uses normal block flow for header/footer and Flexbox only for the three-column middle band.
import './styles.css';
export default function App() {
return (
<main className="container">
<h1>Holy Grail</h1>
<div className="holy-grail">
<header className="hg-header">Header</header>
<div className="hg-middle">
<nav className="hg-nav"><h2>Nav</h2><ul><li>Home</li><li>About</li><li>Contact</li></ul></nav>
<div className="hg-main">
Main content. This column flexes to fill the space between the two
fixed sidebars, and all three share the same height.
</div>
<aside className="hg-aside"><h2>Aside</h2><ul><li>Links</li><li>Ads</li><li>Widgets</li></ul></aside>
</div>
<footer className="hg-footer">Footer</footer>
</div>
</main>
);
}.holy-grail { display: flex; flex-direction: column; gap: 12px; }
.hg-middle { display: flex; align-items: stretch; gap: 12px; min-height: 220px; width: 100%; min-width: 0; }
.hg-nav, .hg-aside { flex: 0 0 150px; }
.hg-main { flex: 1; min-width: 0; }Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Build the holy grail layout — the classic page shape with a full-width header on top, a full-width footer on the bottom, and three columns in between: a navigation sidebar, a flexible main content area, and a secondary sidebar. It earned its name because, for years, doing it well (fluid centre, fixed sidebars, equal heights, sensible source order) was genuinely hard with the tools of the time. With CSS Grid it's now a few lines — which is exactly what this exercise teaches.
// A self-contained, presentational component. No props, no state.
function App(): JSX.Element;
Render five regions — header, nav, main, aside, footer — arranged in the holy-grail shape.
┌──────────────── header ────────────────┐
│ nav │ main │ aside │
└──────────────── footer ────────────────┘
- header & footer span all three columns
- nav & aside are fixed width; main fills the remaining space
- all three middle columns share the same height
grid-template-areas lets you name the five regions and "draw" the layout in CSS — the centre column flexes (1fr) while the sidebars stay fixed.You'll build the page layout that was famously fiddly for a decade — and watch CSS Grid's named areas collapse it into a handful of declarative lines.
Almost every content site has the same skeleton: a banner across the top, a footer across the bottom, and a three-column band in the middle — a nav rail, the main content, and a secondary sidebar. The hard parts historically were making the centre column fluid while the rails stayed fixed, keeping all three columns the same height, and not having to put the sidebars before the content in the HTML. Grid solves all three at once: you name the regions and place them on a template.
Picture a 3×3 grid. The top row is one wide cell (the header), the bottom row is one wide cell (the footer), and the middle row splits into three cells (nav, main, aside). With grid-template-areas you literally draw that map as strings, then tell each region which named area it belongs to. The columns are sized 150px 1fr 150px — fixed rails, fluid centre — and because grid cells in a row stretch to the tallest, the columns are equal height automatically.
Before Grid, the go-to was floating the three middle columns:
<div className="middle">
<nav style={{ float: 'left', width: 150 }}>…</nav>
<aside style={{ float: 'right', width: 150 }}>…</aside>
<div className="main">…</div> {/* fills the middle */}
</div>
This works only with care and quickly bites back. Floated columns don't share a height, so the sidebars stop short of the content (or vice-versa) and their backgrounds don't reach the footer. You need a clearfix so the footer doesn't ride up into the floats, and to avoid the centre slipping under a sidebar you often have to reorder the HTML — sidebars before the content — which hurts reading order and accessibility. Floats were never meant for page layout.
import './styles.css';
export default function App() {
return (
<main className="container">
<h1>Holy Grail</h1>
{/* One grid; each child is placed onto a named area. */}
<div className="holy-grail">
<header className="hg-header">Header</header>
<nav className="hg-nav">
<h2>Nav</h2>
<ul><li>Home</li><li>About</li><li>Contact</li></ul>
</nav>
<div className="hg-main">
Main content. This column flexes to fill the space between the two
fixed sidebars, and all three share the same height.
</div>
<aside className="hg-aside">
<h2>Aside</h2>
<ul><li>Links</li><li>Ads</li><li>Widgets</li></ul>
</aside>
<footer className="hg-footer">Footer</footer>
</div>
</main>
);
}
.holy-grail {
display: grid;
grid-template-areas:
'header header header'
'nav main aside'
'footer footer footer';
grid-template-columns: 150px 1fr 150px; /* fixed | fluid | fixed */
grid-template-rows: auto minmax(220px, 1fr) auto;
gap: 12px;
}
.hg-header { grid-area: header; }
.hg-nav { grid-area: nav; }
.hg-main { grid-area: main; }
.hg-aside { grid-area: aside; }
.hg-footer { grid-area: footer; }
The shift from floats: instead of nudging boxes left and right and cleaning up afterwards, you declare the whole map once. grid-template-areas names the five regions; grid-template-columns: 150px 1fr 150px makes the rails fixed and the centre fluid; equal heights and the spanning header/footer fall out of the grid model for free. The HTML can stay in natural reading order — header, nav, main, aside, footer — regardless of where each lands visually.
There's no runtime logic — the browser resolves the layout from the grid declarations. Say the container is 900px wide with gap: 12px:
150px rails take 300px; two gaps take 24px; the 1fr main column absorbs the remaining 900 − 300 − 24 = 576px.auto (as tall as its content), the footer row is auto, and the middle row is minmax(220px, 1fr) — at least 220px, growing if there's space.grid-area matches a name in the template, so <header> fills the full top row, nav/main/aside fill the middle three cells, and <footer> fills the full bottom row — no matter their order in the HTML.Widen or narrow the container and only the centre column changes width — the rails stay put and the columns stay aligned.
1fr so it absorbs whatever's left.grid-template-areas and a grid-area silently drops the region into an implicit track. Fix: keep the names identical.grid-template-areas rows. Every row string must list the same number of columns, or the template is invalid and ignored. Fix: keep all rows the same column count.grid-template-areas in a media query to stack all five regions into one column.Grid describes the whole page at once; this alternative uses normal block flow for header/footer and Flexbox only for the three-column middle band.
import './styles.css';
export default function App() {
return (
<main className="container">
<h1>Holy Grail</h1>
<div className="holy-grail">
<header className="hg-header">Header</header>
<div className="hg-middle">
<nav className="hg-nav"><h2>Nav</h2><ul><li>Home</li><li>About</li><li>Contact</li></ul></nav>
<div className="hg-main">
Main content. This column flexes to fill the space between the two
fixed sidebars, and all three share the same height.
</div>
<aside className="hg-aside"><h2>Aside</h2><ul><li>Links</li><li>Ads</li><li>Widgets</li></ul></aside>
</div>
<footer className="hg-footer">Footer</footer>
</div>
</main>
);
}.holy-grail { display: flex; flex-direction: column; gap: 12px; }
.hg-middle { display: flex; align-items: stretch; gap: 12px; min-height: 220px; width: 100%; min-width: 0; }
.hg-nav, .hg-aside { flex: 0 0 150px; }
.hg-main { flex: 1; min-width: 0; }Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.