Practice: rebuilding three real layouts
Reading about Flexbox and Grid is not learning them. This lesson is three real layouts, built from scratch, each chosen because it forces a decision you have just read about. Type them; do not copy them.
Start each one from a file with the reset from module 2 and nothing else.
Layout 1: a shop header that survives a phone
The one every site needs, and the one most often broken at 360px.
The brief: a logo on the left, navigation on the right, a phone number after it. On a narrow screen the nav wraps below the logo rather than squashing. The header sticks to the top when you scroll.
<header class="site-header">
<a class="logo" href="/">Sharma Kirana</a>
<nav aria-label="Main">
<ul class="nav-list">
<li><a href="/">Home</a></li>
<li><a href="/prices/">Prices</a></li>
<li><a href="/about.html">About</a></li>
</ul>
</nav>
<a class="phone" href="tel:+912012345678">020 1234 5678</a>
</header>
Note what the HTML already gives you: a <nav> with a label, a list so a screen
reader announces the item count, and a real tel: link. Layout comes after
structure.
.site-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
padding: 1rem;
background: white;
border-bottom: 1px solid #e5e5e5;
position: sticky;
top: 0;
z-index: var(--z-sticky-header, 200);
}
.logo { font-weight: 700; margin-inline-end: auto; }
.nav-list {
display: flex;
flex-wrap: wrap;
gap: 1rem;
list-style: none;
margin: 0;
padding: 0;
}
The decisions, and why:
margin-inline-end: auto on the logo pushes everything else to the right. That is
better here than justify-content: space-between, because with three children
space-between would spread all three apart — the nav would float in the middle. One
auto margin gives you "this one on the left, the rest on the right" for any number
of children.
flex-wrap: wrap on the header and on the nav list. Without it the links squash to
unreadable slivers at 360px, because nowrap is the default.
background: white on a sticky element is not optional — without it the page content
scrolls visibly behind the header.
Things to try:
- Remove
flex-wrapand view at 360px. - Replace the
automargin withjustify-content: space-betweenand see the nav drift. - Remove the
backgroundand scroll. - Add
overflow-x: hiddento a wrapping<div>around the header and watch sticky stop working. That is the module lesson's second sticky trap, and it is how it happens in real projects. - Tab through the header and confirm the focus order matches the visual order.
Layout 2: a product grid with cards that line up
The brief: as many cards per row as fit, minimum 260px. Each card has an image, a title of one or two lines, a description of varying length, and a price pinned to the bottom. Every price must sit on the same line across the row, regardless of how much description each card has.
That last requirement is the point of the exercise.
<ul class="cards">
<li class="card">
<img src="images/atta.jpg" alt="A 10kg bag of atta" width="600" height="400">
<div class="card__body">
<h3 class="card__title">Chakki Fresh Atta</h3>
<p class="card__desc">Stone-ground wholewheat flour, milled weekly.</p>
<p class="card__price">₹450 <span class="card__unit">/ 10kg</span></p>
</div>
</li>
<!-- more cards -->
</ul>
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
list-style: none;
margin: 0;
padding: 0;
}
.card {
display: flex;
flex-direction: column;
border: 1px solid #e5e5e5;
border-radius: 8px;
overflow: hidden;
background: white;
}
.card img {
width: 100%;
aspect-ratio: 3 / 2;
object-fit: cover;
}
.card__body {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
flex: 1;
}
.card__price {
margin-top: auto;
font-weight: 700;
}
The decisions:
Grid for the collection, Flexbox inside the card — the pattern from the previous lesson. Grid gives equal-width columns; Flexbox lets the price be pushed down.
flex: 1 on .card__body makes it absorb the card's spare height, and
margin-top: auto on the price pushes it to the bottom of that. Both are needed: the
body has to be tall before there is anything for auto to eat.
aspect-ratio: 3 / 2 with object-fit: cover means every image occupies the same
space whatever its real proportions, so titles line up. Without object-fit the
images are squashed, which is the default and looks terrible.
overflow: hidden on the card clips the image's top corners to the card's radius.
list-style: none and zeroed padding, rather than not using a list — the module 1
argument.
Things to try:
- Give one card a three-line description and confirm the prices still align.
- Remove
flex: 1from the body. Watch the prices unalign. - Remove
margin-top: auto. Same. - Switch
auto-fittoauto-filland view with two cards. - Remove
object-fit: coverand use a portrait photo. - Remove
overflow: hiddenand look at the image's corners. - Now the harder version: make the titles align too when one card's title wraps to
two lines and another's does not. Flexbox cannot — try
subgridon the card withgrid-template-rows: subgrid, or restructure so the whole thing is one grid.
Layout 3: the classic page skeleton
The brief: header across the top, sidebar and main content side by side, footer across the bottom. The footer sits at the bottom of the viewport even when the page is nearly empty. On mobile everything is one column, with the sidebar after the main content.
<body class="page">
<header class="page__header">…</header>
<main class="page__main">…</main>
<aside class="page__side">…</aside>
<footer class="page__footer">…</footer>
</body>
The HTML order is main-then-aside deliberately. The mobile order we want is the
DOM order, so mobile needs no rearranging at all, and a screen reader reaches the
content before the sidebar on every screen size. Getting this right in the HTML is
what saves you from the order accessibility problem.
.page {
display: grid;
min-height: 100dvh;
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"side"
"footer";
grid-template-rows: auto 1fr auto auto;
gap: 1.5rem;
}
.page__header { grid-area: header; }
.page__main { grid-area: main; }
.page__side { grid-area: side; }
.page__footer { grid-area: footer; }
@media (min-width: 60rem) {
.page {
grid-template-columns: 1fr 16rem;
grid-template-areas:
"header header"
"main side"
"footer footer";
grid-template-rows: auto 1fr auto;
}
}
The decisions:
Mobile first. The single-column rules are the base, and the two-column layout is
inside min-width. That is the module 4 argument arriving early: the simpler layout is
the default, and the media query adds complexity rather than undoing it.
min-height: 100dvh plus 1fr on the main row is what pins the footer down. The main
row absorbs all spare height, so on a short page the footer is at the bottom of the
screen and on a long page it is below the content. This replaces the old "sticky
footer" hacks entirely.
grid-template-areas rather than line numbers, because the CSS is a readable picture
and the media query is one legible block.
16rem rather than 250px for the sidebar, so it scales if the visitor has increased
their font size.
Things to try:
- Load it with one paragraph of content and confirm the footer is at the bottom.
- Change the main row from
1frtoautoand see the footer ride up. - Use
100vhinstead of100dvhon a real phone and scroll. - Give one row string the wrong number of columns and find out what breaks.
- Write the media query the other way round — desktop as the base,
max-widthfor mobile — and compare which stylesheet you would rather maintain. - Put the
<aside>before the<main>in the HTML, keep the same visual result withgrid-template-areas, then tab through the page. That is the accessibility cost of reordering visually. - Add a sticky sidebar with
position: sticky; top: 1reminside a tall page.
The harder exercise
Rebuild the header, the card grid and the skeleton as one page, then:
- Check it at 320px, 375px, 768px and 1440px.
- Tab through the whole thing with no mouse. Everything reachable, in a sensible order, with a visible focus ring.
- Run the horizontal-scrollbar console snippet from the box-model lesson. Fix anything it finds.
- Throttle to Slow 4G and reload. Note what you waited for.
- Switch off the stylesheet entirely and check it still reads as a document.
That last check is the module 1 test, and it should still pass. If the layout is carrying meaning that the HTML does not, this is where you find out.
Check your work
Why one auto margin beats space-between with three children. space-between
spreads all three; an auto margin gives "this one left, the rest right".
Why flex-wrap: wrap on both the header and the nav list. nowrap is the default
and the links squash rather than wrap.
Why a sticky element needs a background. Content scrolls visibly behind it.
What breaks sticky in a real project. An overflow on an ancestor added for an
unrelated reason.
Why the card needs both flex: 1 and margin-top: auto. The body must be tall
before auto has spare space to eat.
What aspect-ratio plus object-fit: cover buys. Every image occupies the same
space, undistorted, so titles align.
What clips the image to the card's rounded corners. overflow: hidden on the card.
Why the HTML is main-then-aside. So the mobile order is the DOM order and nothing needs reordering.
What pins the footer to the bottom. min-height: 100dvh plus 1fr on the main
row.
Why mobile first. The media query adds layout rather than undoing it.
Why 16rem rather than 250px for a sidebar. It scales with the visitor's font
size.
Practice
- Build all three layouts from scratch without looking at the CSS above. Check afterwards.
- Do every "Things to try" item. They are the actual lesson.
- Build the combined page and run the five checks in the harder exercise.
- Rebuild layout 2 using Grid inside the card instead of Flexbox. Which reads better?
- Rebuild layout 3 with Flexbox instead of Grid and list what got worse.
- Add a fourth layout of your own: a two-column article with a floated inset image and a pull quote.
- Find a header on a real site that breaks at 360px. Work out which declaration is missing.
Official documentation
- MDN — CSS layout cookbook — Recipes for these patterns and a dozen more, each with the reasoning for its method.
- MDN — Realising common layouts using grids — The page skeleton and card grid, built step by step.
- MDN — Sticky footers — The footer-at-the-bottom problem and the modern answers to it.
- web.dev — Learn CSS: Layout — A second explanation of the same ground, useful when one wording does not land.
You can now lay out a page: normal flow, Flexbox, Grid, positioning, and the stacking rules that explain the bug nobody can find. Those five cover essentially every layout you will be asked for.
Next module: responsive design — making all of this work on a phone deliberately rather than by accident.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship