Responsive layout with Flexbox and Grid: which one, when, and a page you can build
Flexbox for one-dimensional rows and columns, Grid for two-dimensional page layout, how to choose between them, and a mobile-first page layout built step by step with no media query until it is actually needed.
Flexbox and Grid are the two modern CSS layout systems, and the most common beginner question is which to use. The short answer: Flexbox arranges things along one line (a row or a column) and is good at distributing space between items whose sizes you do not know; Grid arranges things in rows and columns at once and is good at page-level layout where the structure is known. This tutorial builds a typical page, header, content, sidebar and footer, using each where it fits, and explains the properties as they come up.
Start from the HTML
Layout follows structure, so write the markup first. This uses the semantic elements from the previous tutorial.
<body>
<header class="site-header">
<a class="logo" href="/">Site</a>
<nav><a href="/">Home</a><a href="/posts/">Posts</a><a href="/about/">About</a></nav>
</header>
<div class="layout">
<main>
<h1>Title</h1>
<p>Content...</p>
</main>
<aside>Sidebar</aside>
</div>
<footer>Footer</footer>
</body>Mobile first means the default styles, with no media query, are the narrow-screen layout: everything in one column. Wider screens then add columns. Doing it this way round keeps the base CSS simple and means small devices download fewer overrides.
Flexbox for the header
The header is one row: logo on the left, links on the right. That is a one-dimensional problem, so Flexbox.
.site-header {
display: flex;
align-items: center; /* vertical centring in the row */
justify-content: space-between;
gap: 1rem;
padding: .75rem 1rem;
}
.site-header nav {
display: flex;
gap: .25rem 1rem; /* row-gap column-gap */
flex-wrap: wrap; /* links drop to a second line when tight */
}Three properties do most of the work:
justify-contentdistributes items along the main axis (horizontal in a row).space-betweenpushes the first and last to the edges.align-itemsaligns along the cross axis.centerlines up the logo and the links vertically regardless of their heights.gapputs space between items without margins on the items themselves, so there is no stray margin at the ends.
flex-wrap: wrap is the mobile-first safety net: when the links no longer fit on one line they wrap instead of overflowing the viewport.
Grid for the page
The content-plus-sidebar area is two-dimensional on wide screens (two columns, rows determined by content) and one column on narrow screens. Grid handles both with one property change.
.layout {
display: grid;
gap: 2rem;
padding: 1rem;
max-width: 1100px;
margin: 0 auto;
}
@media (min-width: 48em) {
.layout {
grid-template-columns: minmax(0, 2fr) minmax(16rem, 1fr);
}
}With no grid-template-columns, a grid has one column and every child becomes a row: exactly the narrow layout. At 48em (768px at the default font size) the media query adds two columns. The fr unit shares out the free space: 2fr 1fr gives the main column two thirds. The minmax(0, 2fr) on the main column is not decoration: a grid track's default minimum is auto, which is the content's minimum width, so a long unbreakable string such as a URL or a wide code block would force the column wider than its share and push the sidebar off the page. minmax(0, ...) lets the column shrink and the content overflow inside it instead.
Why em in the media query instead of px? If a reader sets their browser's default font size to 20px, 48em becomes 960px and the two-column layout waits until there is really room for the larger text. Pixel breakpoints ignore that.
Cards: Grid without a media query
A grid of cards that fits as many as the width allows is the one place where Grid needs no breakpoint at all:
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
gap: 1rem;
}Read it from the inside out. Each column is at least 16rem and at most one fraction of the space; auto-fill creates as many such columns as fit. The min(100%, 16rem) guards against a container narrower than 16rem (small phones with large text), where a plain 16rem minimum would overflow. One line, works at every width.
Flexbox inside each card
Cards in a row should have their footers aligned even when the text lengths differ. Grid has already made every card in a row the same height (grid items stretch by default). Inside the card, Flexbox in column direction pushes the footer down:
.card {
display: flex;
flex-direction: column;
border: 1px solid #ccc;
padding: 1rem;
}
.card .meta { margin-top: auto; } /* takes all free space above it */margin-top: auto on a flex item absorbs the leftover space, so the element sits at the bottom. The same trick with margin-left: auto in a row pushes one item to the right.
Choosing between them
| Situation | Use |
|---|---|
| Items in a line whose sizes come from their content (nav links, tag chips, toolbar buttons) | Flexbox |
| Centring one thing in a box | Either: display: grid; place-items: center is shortest |
| Page regions that must line up in both directions (header, sidebar, main, footer) | Grid |
| Equal-width columns of cards that wrap | Grid with auto-fill |
| Form rows: label beside input, with wrapping | Flexbox with flex-wrap |
| Overlapping elements (a caption on an image) | Grid: put both in the same cell with grid-area: 1 / 1 |
They nest freely. A Grid cell can contain a flex container and vice versa, and that is how most real pages are built.
Pitfalls
- Fixed heights. Setting
height: 100vhon the layout and then adding content gives you overflow. Usemin-heightand let content decide. - Forgetting
min-width: 0on flex items. A flex item will not shrink below its content's minimum width unless you setmin-width: 0(oroverflow: hidden). This is the Flexbox version of the Gridminmax(0, ...)problem above, and it is the usual cause of a flex row that overflows on phones. - Margins for spacing between items. Use
gap. Margins on items leave a gap at the edge and double up when items wrap. - Reordering visually only.
orderandgrid-areachange where things appear, not the tab order or reading order. Keep the source order sensible. - Percentage widths from the float era.
width: 33.33%withfloat: leftstill works, but it fights with padding and gap. Let Grid compute the columns.
Exercise
Build the page above with the CSS shown, then add a third region: a full-width "featured" strip between the header and the two-column area. On wide screens make the sidebar stick to the top as the page scrolls (position: sticky; top: 1rem; align-self: start on the aside). Resize the window from 320px to 1400px and check that nothing overflows horizontally; the browser's device toolbar in developer tools lets you drag the width.
Summary
- Flexbox: one axis, content-sized items,
justify-content,align-items,gap,flex-wrap. - Grid: two axes,
grid-template-columnswithfrandminmax,repeat(auto-fill, ...)for responsive card grids. - Mobile first: default styles are the single-column layout;
min-widthmedia queries inemadd columns. - The two overflow traps:
minmax(0, 1fr)for grid tracks andmin-width: 0for flex items.