CSS Module 10 – Flexbox
Flexbox patterns for real interfaces
Build bars, media objects, tag lists, input groups, sticky footers and a header that switches layout without media queries, and see when grid does better.
What you will learn
- Build common components with flexbox: navigation bars, media objects, tag lists, input groups and footers
- Design components that adapt without media queries, using flex-wrap and flex-basis
- Recognise when a pattern needs grid instead
Before you start
What you’ll learn · Before you start
What you will learn
- Build common components with flexbox: navigation bars, media objects, tag lists, input groups and footers
- Design components that adapt without media queries, using flex-wrap and flex-basis
- Recognise when a pattern needs grid instead
Before you start
On this page
Most interface parts built with flexbox are one of a few patterns. A bar puts items at both ends of a line, as
in navigation and toolbars. A media object puts a fixed picture beside text that takes the rest of the line. A
wrapping list lets tags or buttons flow onto as many lines as they need. An input group stretches a field
beside its button, a sticky footer keeps the footer at the bottom of short pages, and a switcher turns a row
into a stack when it gets narrow, without a media query. Each needs only two or three declarations from the earlier
lessons: gap, flex: none, flex: 1 with min-inline-size: 0, an auto margin, flex-wrap or a flex-basis.
And each has a trap. This lesson builds all six from one set of tokens, each one drawn at 320, 768 and 1280 px
by Chromium 153, Firefox 155 and WebKit 26.6. The last section shows where flexbox stops and grid does the job better.
One set of tokens
Every pattern below takes its spacing from the same custom properties, so a change of scale happens in one place:
/* The tokens every pattern in this lesson uses: one spacing scale, one radius and one switch width. */
:root {
--space-xs: 0.5rem;
--space-s: 0.75rem;
--space-m: 1rem;
--radius: 0.5rem;
--switch-at: 40rem; /* the width below which the header puts search on its own line */
} Bars with items at both ends
A bar is display: flex, align-items: center and a gap, with one auto margin that splits the line into a start
group and an end group. The fundamentals lesson built a navigation bar this way, measured in English and in Urdu,
and the alignment lesson added align-items: baseline for a heading and a link of different sizes. Two habits keep
bars sturdy. Use the logical margin-inline-start: auto, so the bar mirrors itself in right-to-left text. And add
flex-wrap: wrap to any bar that might run out of room: when the end group no longer fits, it drops to a line of its
own, and its auto margin keeps it at the end of that line, as the header at the end of this lesson shows at 240 px.
Media objects
A media object is a picture and text side by side, often with an action at the end. The picture and the action
keep their size with flex: none, and the text takes the rest with flex: 1:
/* A media object with three parts: a picture and an action that keep their size, and text that takes the rest. */
.media {
display: flex;
align-items: center;
gap: var(--space-s);
}
.media > .picture,
.media > .action {
flex: none;
}
.media > .text {
flex: 1;
min-inline-size: 0; /* long names wrap instead of pushing the action off the line */
overflow-wrap: anywhere;
} <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>A media object with an action</title>
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="media.css">
<style>
body { margin: 0; font: 16px/24px Arial, sans-serif; }
.picture { inline-size: 48px; block-size: 48px; border-radius: 50%; background: #e6edfc; }
.text p { margin: 0; }
.media { margin-block-end: 8px; }
#no-fix > .text { min-inline-size: auto; overflow-wrap: normal; } /* the defaults, for comparison */
</style>
</head>
<body>
<article class="media" id="fixed">
<div class="picture"></div>
<div class="text"><p>Venkatanarasimharajuvaripeta Public Library</p><p>12 new books this week</p></div>
<button class="action" type="button">Follow</button>
</article>
<article class="media" id="no-fix">
<div class="picture"></div>
<div class="text"><p>Venkatanarasimharajuvaripeta Public Library</p><p>12 new books this week</p></div>
<button class="action" type="button">Follow</button>
</article>
<script>
for (const media of document.querySelectorAll('.media')) {
const [picture, text, action] = [...media.children].map((el) => el.getBoundingClientRect());
console.log(`${media.id} at ${innerWidth}px: picture ${picture.width}px, text ${+text.width.toFixed(2)}px, action ${+action.width.toFixed(2)}px ending at ${+action.right.toFixed(2)}px; sticks out ${media.scrollWidth - media.clientWidth}px`);
}
</script>
</body>
</html> The library’s name contains a 28-letter word, Venkatanarasimharajuvaripeta, which is 216 px wide in Arial at 16 px.
At 320 px the text column had only 193 to 198 px of room (the Follow button is a few pixels wider in Chromium than
in WebKit). With min-inline-size: 0 and overflow-wrap: anywhere the word broke and everything fitted in all
three engines. Without them the column refused to shrink below the word, and the Follow button ended 18 px
(WebKit), 20 px (Firefox) or 23 px (Chromium) past the right edge of the screen. At 768 and 1280 px both versions
fitted, so the bug shows only on narrow screens. The sizing lesson explains why: a flex item’s default minimum
width is its longest unbreakable content.
Wrapping lists: tags, and the space-between trap
A list of tags is a wrapping flex container with a gap, and nothing else: each line is packed to the start, and the
gaps between tags and between lines stay equal:
/* A list of tags that wraps: even gaps between tags and between lines, every line packed to the start. */
.tags {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
margin: 0;
padding: 0;
list-style: none;
}
.tags li {
padding: 0.25rem var(--space-s);
border-radius: var(--radius);
}
/* A filter bar: the tags at the start, "Clear all" pushed to the end of whichever line it lands on. */
.tags .clear {
margin-inline-start: auto;
} The trap is justify-content: space-between, which looks right as long as every line is full. Each line is aligned
on its own, so a short last line is spread across the whole width. The example page shows eight 88 px tags with 8 px
gaps four ways, side by side:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tags: packed lines and the space-between trap</title>
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="tags.css">
<style>
body { margin: 0; font: 16px/24px Arial, sans-serif; }
.tags { margin-block-end: 16px; }
.tags li { box-sizing: border-box; inline-size: 88px; background: #e6edfc; } /* equal tags, so every engine agrees */
.tags li.clear { inline-size: auto; }
#spread { justify-content: space-between; }
#grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); }
</style>
</head>
<body>
<ul class="tags" id="packed"><li>Fiction</li><li>Poetry</li><li>History</li><li>Science</li><li>Travel</li><li>Cooking</li><li>Comics</li><li>Music</li></ul>
<ul class="tags" id="spread"><li>Fiction</li><li>Poetry</li><li>History</li><li>Science</li><li>Travel</li><li>Cooking</li><li>Comics</li><li>Music</li></ul>
<ul class="tags" id="grid"><li>Fiction</li><li>Poetry</li><li>History</li><li>Science</li><li>Travel</li><li>Cooking</li><li>Comics</li><li>Music</li></ul>
<ul class="tags" id="filters"><li>Fiction</li><li>Poetry</li><li>History</li><li>Science</li><li class="clear"><a href="#clear">Clear all</a></li></ul>
<script>
for (const list of document.querySelectorAll('.tags')) {
const left = list.getBoundingClientRect().x;
const lines = new Map();
for (const li of list.children) {
const r = li.getBoundingClientRect();
lines.set(r.y, [...(lines.get(r.y) ?? []), r]);
}
const desc = [...lines.values()].map((rs, i) => {
const gaps = rs.slice(1).map((r, j) => +(r.x - rs[j].right).toFixed(2));
return `line ${i + 1}: ${rs.length} tags at x ${rs.map((r) => +(r.x - left).toFixed(2)).join(', ')}${gaps.length ? `, gaps ${gaps.join(', ')}` : ''}`;
});
console.log(`${list.id} at ${innerWidth}px | ${desc.join(' | ')}`);
}
</script>
</body>
</html> At 320 px the tags fill lines of 3, 3 and 2, and at 600 px lines of 6 and 2. The table gives the space between the two tags on the last line:
| Screen | Packed | space-between |
|---|---|---|
| 320 px | 8 px | 144 px |
| 600 px | 8 px | 424 px |
The full lines looked fine with space-between (gaps of 28 px at 320 px, 14.4 px at 600 px); only the last line
broke apart, its two tags drawn at 0 and 232 px, then at 0 and 512 px. In the grid the last two tags started at 0 and
109.33 px (101.33 px at 600 px), under the first two columns. All three engines placed the tags identically, give or take 1/64 px, and the arithmetic needs only the
line-breaking rule from the fundamentals lesson:
// Why justify-content: space-between spoils the last line of a wrapping list. Flexbox fills each line with as many
// items as fit (counting the gaps), then aligns every line on its own. These are the eight 88px tags with 8px gaps
// of tags.html; Chromium 153, Firefox 155 and WebKit 26.6 put them at these positions.
function lines(count, size, gap, width) {
const out = [];
let line = 0;
for (let i = 0; i < count; i++) {
const needed = line === 0 ? size : line * (size + gap) + size;
if (line > 0 && needed > width) {
out.push(line);
line = 0;
}
line++;
}
out.push(line);
return out;
}
function positions(n, size, gap, width, justify) {
const free = width - n * size - (n - 1) * gap;
const extra = justify === 'space-between' && n > 1 ? free / (n - 1) : 0;
return Array.from({ length: n }, (_, i) => Math.round(i * (size + gap + extra) * 100) / 100);
}
const [count, size, gap] = [8, 88, 8];
console.log(`${count} tags of ${size}px with ${gap}px gaps`);
for (const width of [320, 600]) {
const perLine = lines(count, size, gap, width);
const last = perLine.at(-1);
console.log(`${width}px: lines of ${perLine.join(', ')}`);
for (const justify of ['flex-start', 'space-between']) {
const xs = positions(last, size, gap, width, justify);
console.log(` ${justify.padEnd(13)} last line at x ${xs.join(', ')}, a gap of ${Math.round((xs[1] - xs[0] - size) * 100) / 100}px`);
}
} Output
8 tags of 88px with 8px gaps 320px: lines of 3, 3, 2 flex-start last line at x 0, 96, a gap of 8px space-between last line at x 0, 232, a gap of 144px 600px: lines of 6, 2 flex-start last line at x 0, 96, a gap of 8px space-between last line at x 0, 512, a gap of 424px
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node last_line.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The right fix depends on what the list should do. When the tags should simply flow, keep the default packing and let
gap do the spacing. When they should form columns, use a grid: grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)) gave every line the same columns. And when one item belongs at the end, such as a “Clear all”
link in a filter bar, give that item margin-inline-start: auto: at 320 px it ended at the right edge of its line,
whichever line it landed on.
Input groups
An input with its button is a flex row in which the input takes the remaining space. The trap is the input’s own
width. The HTML standard gives a text field a default width of 20 characters, or as many as its size attribute
asks for, worked out from the font’s average and widest characters, and in all three engines the field would not
shrink below that width as a flex item:
/* An input with its button: the input takes the rest of the line, the button keeps its size. */
.input-group {
display: flex;
gap: var(--space-xs);
}
.input-group input {
flex: 1;
min-inline-size: 0; /* an input's automatic minimum is its default width: let it go below that */
}
.input-group button {
flex: none;
} <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>An input group in a narrow card</title>
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="input-group.css">
<style>
body { margin: 0; font: 16px/24px Arial, sans-serif; }
.card { inline-size: 240px; margin-block-end: 8px; } /* a card on a 320px phone: 320 minus 2 x 40px of padding */
#default-min input { min-inline-size: auto; } /* the default, for comparison */
</style>
</head>
<body>
<form class="card input-group" id="with-min"><input type="email" aria-label="Email address"><button type="submit">Get updates</button></form>
<form class="card input-group" id="default-min"><input type="email" aria-label="Email address"><button type="submit">Get updates</button></form>
<script>
for (const form of document.querySelectorAll('.input-group')) {
const input = form.querySelector('input').getBoundingClientRect();
const button = form.querySelector('button').getBoundingClientRect();
const right = form.getBoundingClientRect().right;
console.log(`${form.id}: input ${+input.width.toFixed(2)}px, button ${+button.width.toFixed(2)}px, button ends ${+(button.right - right).toFixed(2)}px past the card`);
}
</script>
</body>
</html> The table gives the field’s default width and how far the button stuck out of the card:
| Engine | Input | Overflow |
|---|---|---|
| Chromium 153 | 153 px | 9.63 px |
| Firefox 155 | 189 px | 44.97 px |
| WebKit 26.6 | 155.88 px | 4.09 px |
The default widths of the same email field differed by 36 px between engines, so a group that fitted in WebKit
pushed the button 45 px out of the card in Firefox. min-inline-size: 0 on the input fixed it in all three: the
input became 143.38 px wide in Chromium, 144.03 px in Firefox and 151.78 px in WebKit, and the button fitted. flex: none on the button kept its label on one line.
A sticky footer
A footer that sits at the bottom of the screen on a short page, and after the content on a long one, needs no
positioning at all. Make the body a column flex container at least as tall as the screen, and let main take every
spare pixel:
/* Footer at the bottom of the screen when the page is short, after the content when it is long. */
body {
display: flex;
flex-direction: column;
min-block-size: 100dvh; /* the visible height, also while a phone's toolbars change size */
margin: 0;
}
main {
flex: 1; /* takes every pixel the header and footer leave */
} <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>A footer that stays at the bottom</title>
<link rel="stylesheet" href="sticky-footer.css">
<style>
body { font: 16px/24px Arial, sans-serif; }
header, footer { padding: 8px; background: #e6edfc; }
</style>
</head>
<body>
<header>Lotus Books</header>
<main><p>Your basket is empty.</p></main>
<footer>Contact · Returns · Privacy</footer>
<script>
// ?long adds forty paragraphs, so the page is taller than the screen.
if (location.search.includes('long')) {
for (let i = 1; i <= 40; i++) document.querySelector('main').insertAdjacentHTML('beforeend', `<p>Line ${i}</p>`);
}
const footer = document.querySelector('footer').getBoundingClientRect();
const main = document.querySelector('main').getBoundingClientRect();
console.log(`${innerWidth} x ${innerHeight}: main ${+main.height.toFixed(2)}px tall, footer bottom at ${+(footer.bottom + scrollY).toFixed(2)}px of a ${document.documentElement.scrollHeight}px page`);
</script>
</body>
</html> On a 740 px tall screen at 320, 768 and 1280 px wide, the short page’s main grew to 660 px and the footer’s bottom
edge sat exactly at 740 px in all three engines. With forty more paragraphs the page grew to 1,736 px and the footer
followed the content. min-block-size: 100dvh uses the dynamic viewport height, which follows a phone browser’s
toolbars as they appear and disappear; the viewport units lesson compares it with vh. Use a minimum, never
block-size: 100vh, or long pages overflow their own body.
A header that switches without a media query
The last pattern decides its layout from the space it has. A site header shows the logo, the links and a search
form on one line when it is at least 40rem (640 px) wide, and gives search a line of its own below that. One
flex-basis does the switching:
/* Site header: logo, links and search on one line when the header is at least --switch-at wide; below that,
search gets a line of its own. No media query and no container query: flex-basis does the switching. */
.site-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-m);
}
.site-header .logo,
.site-header .links {
flex: none;
}
.site-header .links {
margin-inline-start: auto; /* on a line of its own with the logo, the links go to the end */
}
.site-header .search {
/* Wider than --switch-at: a negative basis, clamped to 0, so search shares the line and grows into it.
Narrower: a basis of at least 999px, so search can share a line with nothing and wraps. */
flex: 1 1 calc((var(--switch-at) - 100%) * 999);
min-inline-size: 0;
} <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>A header that switches without a media query</title>
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="header.css">
<style>
body { margin: 0; font: 16px/24px Arial, sans-serif; }
/* Fixed widths, so every engine lays out the same boxes. */
.logo { inline-size: 120px; }
.links { inline-size: 136px; display: flex; gap: 8px; margin-block: 0; padding: 0; list-style: none; }
.search input { box-sizing: border-box; inline-size: 100%; }
</style>
</head>
<body>
<header class="site-header">
<a class="logo" href="#home">Lotus Books</a>
<ul class="links"><li><a href="#shop">Shop</a></li><li><a href="#blog">Blog</a></li><li><a href="#help">Help</a></li></ul>
<form class="search" role="search"><input type="search" name="q" aria-label="Search books"></form>
</header>
<script>
const header = document.querySelector('.site-header');
const headerTop = header.getBoundingClientRect().y;
const lines = new Map();
for (const el of header.children) {
const r = el.getBoundingClientRect();
const key = Math.round(r.y - headerTop + r.height / 2);
lines.set(key, [...(lines.get(key) ?? []), `${el.className} ${+r.width.toFixed(2)}px at x ${+r.x.toFixed(2)}`]);
}
const search = getComputedStyle(header.querySelector('.search'));
console.log(`${innerWidth}px: ${[...lines.values()].map((l, i) => `line ${i + 1}: ${l.join(', ')}`).join(' | ')} | search flex-basis ${search.flexBasis}`);
</script>
</body>
</html> The basis calc((40rem - 100%) * 999) is worked out from the header’s width. At 640 px or more, 40rem minus 100% is
zero or negative; a flex basis cannot be negative, so the browser clamps the result to 0, and a search form of
basis 0 fits on the line and grows into the space left. Below 640 px the result is positive and multiplied by 999:
at 639 px it is already 999 px, wider than the whole header, so search cannot share a line with anything, wraps, and
shrinks to the header’s width. All three engines kept the calc() for the computed value, written as
calc(-99900% + 639360px), and switched at exactly the same pixel:
| Width | Line 1 | Line 2 | Search |
|---|---|---|---|
| 1280 px | all three | 992 px | |
| 768 px | all three | 480 px | |
| 640 px | all three | 352 px | |
| 639 px | logo, links | search | 639 px |
| 480 px | logo, links | search | 480 px |
| 320 px | logo, links | search | 320 px |
| 240 px | logo | links | 240 px |
Below 640 px the links’ auto margin moved them to the end of the first line. At 240 px even the logo and the links could not share a line, so the links took the second line and search a third.
The header one pixel either side of its switch width
Text description of the diagram
The diagram shows the lesson's site header, a wrapping flex container with a 120px logo, 136px of links and a search form whose flex-basis is calc((640px - 100%) * 999), at half scale.
- At 640px wide, the basis works out to (640 − 640) × 999 = 0. A basis of 0 fits on the line after the logo and the links, so all three share one line, and the search form grows into the rest of it, 352px.
- At 639px wide, the basis is (640 − 639) × 999 = 999px, more than the whole header, so the search form cannot share a line with anything. It wraps to a second line and shrinks to the header's width, 639px. On the first line, the links' auto margin moves them to the end.
The trick has a cost: the threshold hides inside arithmetic that the next reader has to decode. A container query
says the same thing in plain words, and container size queries are Baseline widely available, from Chrome 105,
Firefox 110 and Safari 16. The header becomes the query container with container-type: inline-size, and the query
restyles its search form:
/* The same header with a container query. The header is the query container (container-type: inline-size), and
below 40rem its search form takes a full line. Easier to read than the calc() switch. */
.site-header {
container-type: inline-size;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-m);
}
.site-header .logo,
.site-header .links {
flex: none;
}
.site-header .links {
margin-inline-start: auto;
}
.site-header .search {
flex: 1 1 0;
min-inline-size: 0;
}
@container (inline-size < 40rem) {
.site-header .search {
flex-basis: 100%;
}
} <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>The header with a container query</title>
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="header-cq.css">
<style>
body { margin: 0; font: 16px/24px Arial, sans-serif; }
.logo { inline-size: 120px; }
.links { inline-size: 136px; display: flex; gap: 8px; margin-block: 0; padding: 0; list-style: none; }
.search input { box-sizing: border-box; inline-size: 100%; }
/* A toolbar that takes its width from its buttons, with and without inline-size containment. */
.toolbar { display: inline-flex; gap: 8px; }
.toolbar.contained { container-type: inline-size; }
</style>
</head>
<body>
<header class="site-header">
<a class="logo" href="#home">Lotus Books</a>
<ul class="links"><li><a href="#shop">Shop</a></li><li><a href="#blog">Blog</a></li><li><a href="#help">Help</a></li></ul>
<form class="search" role="search"><input type="search" name="q" aria-label="Search books"></form>
</header>
<div class="toolbar" id="content-sized"><button type="button">Bold</button><button type="button">Italic</button></div>
<div class="toolbar contained" id="contained"><button type="button">Bold</button><button type="button">Italic</button></div>
<script>
const header = document.querySelector('.site-header');
const headerTop = header.getBoundingClientRect().y;
const lines = new Map();
for (const el of header.children) {
const r = el.getBoundingClientRect();
const key = Math.round(r.y - headerTop + r.height / 2);
lines.set(key, [...(lines.get(key) ?? []), `${el.className} ${+r.width.toFixed(2)}px at x ${+r.x.toFixed(2)}`]);
}
console.log(`${innerWidth}px: ${[...lines.values()].map((l, i) => `line ${i + 1}: ${l.join(', ')}`).join(' | ')}`);
for (const bar of document.querySelectorAll('.toolbar')) console.log(`${bar.id} toolbar: ${+bar.getBoundingClientRect().width.toFixed(2)}px wide`);
</script>
</body>
</html> At 320, 639, 640 and 1280 px it drew exactly the same lines and widths as the calc() version, in all three
engines. The query comes with a condition of its own: container-type: inline-size makes a box work out its width
as if it had no content. On the same page, an inline-flex toolbar of two buttons was 94.11 px wide in Chromium
(89.1 px in Firefox, 88.59 px in WebKit), and 0 px wide in all three once it became a container. A header that fills
the width loses nothing, so prefer the query there; keep the flex-basis switch for components that take their width
from their content.
When the pattern needs grid
Flexbox shares out space along one line at a time, and every line decides for itself. That is exactly right for bars,
tags and media objects, and wrong when items must line up in rows and columns at once. Five cards with flex: 1 1 14rem (a 224 px basis) against a grid with repeat(auto-fill, minmax(14rem, 1fr)); the table gives card widths in
px:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Five cards: flex-wrap or grid</title>
<link rel="stylesheet" href="tokens.css">
<style>
body { margin: 0; font: 16px/24px Arial, sans-serif; }
.cards > * { background: #e6edfc; }
#flex { display: flex; flex-wrap: wrap; gap: var(--space-m); margin-block-end: 16px; }
#flex > * { flex: 1 1 14rem; } /* at least 224px, then share the line */
#grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: var(--space-m); }
</style>
</head>
<body>
<div class="cards" id="flex"><article>One</article><article>Two</article><article>Three</article><article>Four</article><article>Five</article></div>
<div class="cards" id="grid"><article>One</article><article>Two</article><article>Three</article><article>Four</article><article>Five</article></div>
<script>
for (const cards of document.querySelectorAll('.cards')) {
const rows = new Map();
for (const card of cards.children) {
const r = card.getBoundingClientRect();
rows.set(r.y, [...(rows.get(r.y) ?? []), +r.width.toFixed(2)]);
}
console.log(`${cards.id} at ${innerWidth}px | ${[...rows.values()].map((w, i) => `row ${i + 1}: ${w.join(', ')}`).join(' | ')}`);
}
</script>
</body>
</html> | Width | Flex | Grid |
|---|---|---|
| 320 px | 320 each | 320 each |
| 768 px | 3 × 245.33, then 2 × 376 | 5 × 245.33 |
| 1280 px | 5 × 243.2 | 5 × 243.2 |
At 320 px every card had a line, or a grid row, of its own.
At 768 px the two cards on flexbox’s second line grew to 376 px, half as wide again as the three above them, because that line had more room to share. The grid kept every card in its column. The rule of thumb: if the content sizes the layout, and each line may differ, use flexbox; if the layout sizes the content, in columns that line up, use grid. The grid lessons that follow this module start from there.
CSS Grid Generator Build the auto-fill card grid from this section and compare it with the flex version.Practise: predict the switching header
Exercise · Medium · JavaScript
Predict the header that switches without a media query
The lesson's site header is a wrapping flex container with a 16 px gap. In HTML order it holds the logo (120 px, flex: none), the links (136 px, flex: none) and the search form, which has flex: 1 1 calc((40rem - 100%) * 999) and min-inline-size: 0; 40rem is 640 px and 100% is the header's width. Write two functions in header.mjs and export them. All lengths are in px.
The first, searchBasis(width), returns the flex basis the search form gets in a header width px wide. A flex basis can't be negative, so a negative result of the calc() becomes 0.
The second, headerLayout(width), returns { lines, search }: the flex lines as arrays of the names 'logo', 'links' and 'search', and the width the search form ends up with. Items go onto a line in HTML order, each counted at its basis, with 16 px between neighbours, until the next one would make the line longer than the header; that item starts a new line, and the first item of a line always goes on it. Then the search form grows or shrinks to fill whatever its own line leaves.
The sample tests import both functions from header.mjs and run in your browser. Their numbers are the ones Chromium 153, Firefox 155 and WebKit 26.6 drew for the lesson's header at seven widths.
Starter code · header.mjs
/** The flex basis of the search form in a header `width` px wide: calc((640px - 100%) * 999), never below 0. */
export function searchBasis(width) {
// Replace this line with your code.
return 0;
}
/** The lines of the header and the width of the search form. */
export function headerLayout(width) {
// Replace this line with your code.
return { lines: [['logo', 'links', 'search']], search: width };
} The sample tests · header.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { searchBasis, headerLayout } from './header.mjs';
test('the basis is 0 from 640px up and huge below it', () => {
assert.equal(searchBasis(1280), 0);
assert.equal(searchBasis(640), 0);
assert.equal(searchBasis(639), 999);
assert.equal(searchBasis(320), 319680);
});
test('wide headers keep everything on one line', () => {
assert.deepEqual(headerLayout(1280), { lines: [['logo', 'links', 'search']], search: 992 });
assert.deepEqual(headerLayout(768), { lines: [['logo', 'links', 'search']], search: 480 });
assert.deepEqual(headerLayout(640), { lines: [['logo', 'links', 'search']], search: 352 });
});
test('below 640px search gets a line of its own', () => {
assert.deepEqual(headerLayout(639), { lines: [['logo', 'links'], ['search']], search: 639 });
assert.deepEqual(headerLayout(480), { lines: [['logo', 'links'], ['search']], search: 480 });
assert.deepEqual(headerLayout(320), { lines: [['logo', 'links'], ['search']], search: 320 });
});
test('a header too narrow for the logo and the links', () => {
assert.deepEqual(headerLayout(240), { lines: [['logo'], ['links'], ['search']], search: 240 });
}); A hint
searchBasis is Math.max(0, (640 - width) * 999). In headerLayout, collect the three items into lines as in the toolbar exercise of the fundamentals lesson, using the search form's basis as its size. On the search form's line, its width is the header's width minus the other items on that line and the gaps between them.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · When would you choose flexbox, and when grid?
Choose flexbox when the content decides the layout along one direction: a navigation bar, a toolbar, a list of tags, a media object. Each flex line shares out its own space, so items with different content can get different widths, and lines that wrap are independent of each other. Choose grid when the layout decides where content goes, in rows and columns that must line up, such as a card grid, a page with a sidebar, header and footer, or a form with aligned labels. A quick test is the last row of a wrapping card list: with flexbox, two cards on a short last line grow wider than the cards above them, 376 px against 245 px at 768 px in this lesson, while a grid keeps them in their columns. The two combine well: a grid for the page, flexbox inside each card.
Key takeaways
- Bars:
align-items: center, agapand one logical auto margin; addflex-wrap: wrapso the end group can drop to its own line. - Media objects and input groups:
flex: noneon the fixed parts,flex: 1andmin-inline-size: 0on the part that flexes. Default input widths differed by 36 px between engines. - Wrapping lists: pack them and space them with
gap;space-betweenspreads a short last line, 424 px apart at 600 px. - Sticky footer: a column flex
bodywithmin-block-size: 100dvhandflex: 1onmain. flex-basis: calc((40rem - 100%) * 999)switches a row to a stack at 40rem without a media query; a container query says the same more readably.- When items must line up in rows and columns, use grid.
References
- CSS Flexible Box Layout Module Level 1: flex-basis (W3C CSS Working Group)
- CSS Flexible Box Layout Module Level 1: collecting flex items into flex lines (W3C CSS Working Group)
- CSS Flexible Box Layout Module Level 1: automatic minimum size of flex items (W3C CSS Working Group)
- CSS Values and Units Module Level 4: range checking of calculations (W3C CSS Working Group)
- CSS Conditional Rules Module Level 5: container queries (W3C CSS Working Group)
- CSS Containment Module Level 3: inline-size containment (W3C CSS Working Group)
- CSS Grid Layout Module Level 2: auto-fill and auto-fit repetitions (W3C CSS Working Group)
- CSS Values and Units Module Level 4: viewport-percentage lengths (W3C CSS Working Group)
- Web features explorer: Container queries (size) (W3C WebDX Community Group)
- HTML Standard: the input element as a text entry widget (WHATWG)
- Typical use cases of flexbox (MDN Web Docs) (Mozilla)
- Relationship of flexbox to other layout methods (MDN Web Docs) (Mozilla)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress