CSS Module 12 – Responsive design: media and container queries
Layouts that adapt without breakpoints
Build six layout patterns that adapt to their space without media queries, give them custom-property knobs, and compose whole pages from them.
What you will learn
- Compose pages from reusable patterns such as a vertical stack, wrapping row, sidebar layout, switch, centred hero and scroller
- Choose between flexbox and grid for each pattern
- Build a small pattern library whose knobs are custom properties
Before you start
What you’ll learn · Before you start
What you will learn
- Compose pages from reusable patterns such as a vertical stack, wrapping row, sidebar layout, switch, centred hero and scroller
- Choose between flexbox and grid for each pattern
- Build a small pattern library whose knobs are custom properties
Before you start
On this page
An intrinsic layout responds to the space it actually gets, through wrapping, flexible sizes and limits such as
min(), instead of switching at viewport widths. Built that way, a component works in a phone, a sidebar and a wide
screen without knowing which one it is in, and most media queries disappear. Six patterns cover most pages: a
vertical stack for anything in a column, a wrapping row for tags and buttons, a sidebar layout that stacks
when the content would get too narrow, a row-to-column switch, a centred hero and a horizontal scroller.
Each is a few lines of CSS with knobs, custom properties that change it without new classes. This lesson builds
the six as a 42-line library, measures each in Chromium 153, Firefox 155 and WebKit 26.6, keeps their knobs from
leaking into each other, and composes a landing page with no media query at all.
Six patterns at a glance
Six patterns, each with the knob that changes it most
Text description of the diagram
The diagram shows six small drawings in two columns, each with its name and its main knob.
- Vertical stack: three bars one above the other with equal space between them. Knob: --stack-gap.
- Wrapping row: rounded chips of different widths on three lines. Knob: --row-gap.
- Sidebar layout: a narrow box beside a wide one; it stacks when the content would get too narrow. Knob: --content-min.
- Row-to-column switch: three equal boxes in one row; all of them stack below the threshold. Knob: --switch-at.
- Centred hero: a tall box with a thin bar at the top, a heading in the middle and a thin bar at the bottom. Knob: --hero-height.
- Horizontal scroller: a frame holding two whole cards and part of a third; it scrolls inside itself, never the page. Knob: --item-width.
The whole library, which the rest of the lesson takes apart:
/* Six layout patterns that adapt to the space they get, with no media query. Each reads its settings, its knobs,
from custom properties, so a page changes a pattern by setting a property on it. */
/* Every pattern sets its own defaults, so a knob set on an outer pattern never leaks into one nested inside it.
:where() has no specificity, so any class that sets a knob wins. */
:where(.stack) { --stack-gap: 1.5rem; }
:where(.row-wrap, .scroller) { --row-gap: 0.75rem; }
:where(.with-sidebar) { --sidebar-width: 16rem; --content-min: 60%; --sidebar-gap: 1.5rem; }
:where(.switch) { --switch-at: 40rem; --switch-gap: 1.5rem; }
:where(.hero) { --hero-height: 100svh; --hero-padding: 1.5rem; }
:where(.scroller) { --item-width: 16rem; }
/* 1. Vertical stack: children in a column; the stack owns the space between them. */
.stack { display: flex; flex-direction: column; gap: var(--stack-gap); }
.stack > * { margin-block: 0; }
/* 2. Wrapping row: a row that wraps onto as many lines as its items need. */
.row-wrap { display: flex; flex-wrap: wrap; align-items: center; gap: var(--row-gap); }
/* 3. Sidebar layout: the sidebar keeps its width until the content would be narrower than --content-min. */
.with-sidebar { display: flex; flex-wrap: wrap; gap: var(--sidebar-gap); }
.with-sidebar > :first-child { flex: 1 1 var(--sidebar-width); }
.with-sidebar > :last-child { flex: 999 1 0; min-inline-size: var(--content-min); }
/* 4. Row-to-column switch: every child in one row from --switch-at, every child stacked below it. */
.switch { display: flex; flex-wrap: wrap; gap: var(--switch-gap); }
.switch > * { flex-grow: 1; flex-basis: calc((var(--switch-at) - 100%) * 999); }
/* 5. Centred hero: at least as tall as the small viewport, its middle child centred, the others at the edges. */
.hero { display: flex; flex-direction: column; min-block-size: var(--hero-height); padding: var(--hero-padding); }
.hero > .hero-middle { margin-block: auto; }
/* 6. Horizontal scroller: one row of items that scrolls inside itself, never the page. */
.scroller {
display: grid;
grid-auto-flow: column;
grid-auto-columns: min(80%, var(--item-width));
gap: var(--row-gap);
overflow-x: auto;
scroll-snap-type: inline mandatory;
}
.scroller > * { scroll-snap-align: start; } | Pattern | Built with | Adapts by | Main knob |
|---|---|---|---|
| vertical stack | flexbox, a column | nothing to adapt: one column at every width | --stack-gap |
| wrapping row | flexbox, wrapping | moving items onto new lines | --row-gap |
| sidebar layout | flexbox, wrapping | putting the content below the sidebar | --content-min |
| row-to-column switch | flexbox, wrapping | stacking every item below one width | --switch-at |
| centred hero | flexbox, a column | growing past its minimum height | --hero-height |
| horizontal scroller | grid, one row | scrolling inside itself | --item-width |
Five of the six use flexbox, because their content decides how much room each part takes. The scroller uses grid, because it needs columns of one fixed width however much content each card has.
The patterns, one by one
The demo page puts all six on one page, and its script prints how each one laid out its children:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Six layout patterns</title>
<link rel="stylesheet" href="patterns.css">
<style>
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font: 16px/1.5 Arial, sans-serif; }
.demo { padding: 1rem; }
.box { padding: 0.5rem; border: 1px solid #767676; }
#hero { --hero-height: 24rem; border: 1px solid #767676; } /* shorter than a screen, to fit the demo */
</style>
</head>
<body>
<main class="demo stack">
<div class="stack" id="stack"><p class="box">One</p><p class="box">Two</p><p class="box">Three</p></div>
<div class="row-wrap" id="row"><span class="box">Cotton</span><span class="box">Silk</span><span class="box">Linen</span><span class="box">Handloom</span><span class="box">Under ₹999</span><span class="box">New this week</span></div>
<div class="with-sidebar" id="sidebar"><nav class="box">Filters</nav><section class="box">Product results</section></div>
<div class="switch" id="switch"><div class="box">Free delivery</div><div class="box">Easy returns</div><div class="box">Cash on delivery</div></div>
<section class="hero" id="hero"><p>Header</p><h1 class="hero-middle">Festive sale</h1><p>Footer</p></section>
<div class="scroller" id="scroller" tabindex="0" role="region" aria-label="Bestsellers"><div class="box">1</div><div class="box">2</div><div class="box">3</div><div class="box">4</div><div class="box">5</div></div>
</main>
<script>
// How each pattern laid out its children: how many rows (distinct tops) and each child's width.
const rows = (els) => new Set(els.map((e) => Math.round(e.getBoundingClientRect().top))).size;
const widths = (els) => els.map((e) => Number(e.getBoundingClientRect().width.toFixed(2))).join(', ');
for (const id of ['stack', 'row', 'sidebar', 'switch', 'scroller']) {
const el = document.getElementById(id);
const kids = [...el.children];
const extra = id === 'scroller' ? ` · scrolls ${el.scrollWidth - el.clientWidth} px inside itself` : '';
console.log(`${id}: ${rows(kids)} row(s) · widths ${widths(kids)}${extra}`);
}
const hero = document.getElementById('hero');
const h1 = hero.querySelector('h1').getBoundingClientRect();
const box = hero.getBoundingClientRect();
console.log(`hero: ${Math.round(box.height)} px tall · heading centre at ${Math.round((h1.top + h1.bottom) / 2 - box.top)} px · page ${document.documentElement.scrollWidth} px wide`);
</script>
</body>
</html> Vertical stack: the stack owns the spacing
A stack is a flex column with a gap. The rule that matters is the second one, which removes its children’s block
margins. Margins don’t collapse in a flex container, so without it they add up with the gap:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Margins and gap in a stack</title>
<style>
body { margin: 0; font: 16px/1.5 Arial, sans-serif; }
p { margin-block: 16px; } /* the margins paragraphs have by default */
.flow { display: block; } /* normal flow: no gap */
.flex { display: flex; flex-direction: column; gap: 24px; } /* a stack that keeps the margins */
.owned { display: flex; flex-direction: column; gap: 24px; } /* a stack that owns the spacing */
.owned > * { margin-block: 0; }
</style>
</head>
<body>
<div class="flow"><p>One</p><p>Two</p></div>
<div class="flex"><p>One</p><p>Two</p></div>
<div class="owned"><p>One</p><p>Two</p></div>
<script>
for (const box of document.body.querySelectorAll('div')) {
const [a, b] = box.children;
console.log(`${box.className}: ${Math.round(b.getBoundingClientRect().top - a.getBoundingClientRect().bottom)} px between the paragraphs`);
}
</script>
</body>
</html> | Container | Space between two paragraphs |
|---|---|
| normal flow, no gap | 16 px: the two 16 px margins collapse into one |
flex column, gap: 24px |
56 px: 16 + 24 + 16 |
flex column, gap: 24px, margins removed |
24 px |
All three engines measured the same. A stack that owns its spacing makes components portable: a card needs no margin of its own, because whichever layout holds it decides the space around it.
Wrapping row
A wrapping row is a flex container with flex-wrap: wrap and a gap, for tags, filters and buttons. On the demo page
six filters fit on one line at 768 px and took three lines at 320 px, with the same 0.75rem between items and between
lines. The flexbox patterns lesson measured its one trap, justify-content: space-between, which spreads a short last
line across the whole width.
Sidebar layout: wrap when the content gets too narrow
The sidebar layout has two children. The sidebar asks for --sidebar-width (16rem); the content asks for nothing,
grows 999 times faster than the sidebar, and may never be narrower than --content-min (60 %) of the container. They
share a line while the sidebar, the gap and 60 % of the container fit in it; with the 16rem sidebar and the 1.5rem gap
that is from a 700 px container:
| Container | Layout | Sidebar | Content |
|---|---|---|---|
| 288 px | content below the sidebar | 288 px | 288 px |
| 736 px | side by side | 256.43 px | 455.57 px |
| 1248 px | side by side | 256.95 px | 967.05 px |
The three engines agreed to a hundredth of a pixel. The sidebar ends up slightly wider than 256 px because it gets one thousandth of the free space; the content gets the rest. The threshold follows the container, not the window, so the same layout works in a page column and in a full-width band.
Row-to-column switch
The switch puts every child in one row from --switch-at (40rem) and stacks all of them below it, never two above
one. Each child has flex-grow: 1 and the basis calc((var(--switch-at) - 100%) * 999), which is 0 when the
container is wide enough and enormous when it isn’t; the flexbox patterns lesson works through that arithmetic. On the
demo page the three panels were 229.33 px each in a 736 px container, 400 px each at 1248 px, and stacked at 288 px.
A container query such as @container (width >= 40rem) says the same in plain words, at the price of a container,
which must get its width from outside, as the container queries lesson showed.
Centred hero
The hero is a flex column at least --hero-height tall, 100svh by default, the small viewport height, which stays
visible when a phone’s browser shows its toolbars. Its middle child has margin-block: auto, which takes the free
space above and below it, so a header line sits at the top, the heading in the middle and a footer line at the
bottom. On the demo page, set to 24rem, the hero was 384 px tall with the heading’s centre at 192 px, in every engine
and at every width. Because the height is a minimum, longer text makes the hero taller instead of overflowing it.
Horizontal scroller
The scroller is a grid with grid-auto-flow: column, so every child becomes a column in one row, and
grid-auto-columns: min(80%, var(--item-width)), so a card is 16rem unless that is more than 80 % of the scroller.
With overflow-x: auto it scrolls inside itself, and the page around it never scrolls sideways:
| Scroller | Card width | Scrolls inside itself |
|---|---|---|
| 288 px | 230.4 px, 80 % of the scroller | 912 px |
| 736 px | 256 px | 592 px |
| 1248 px | 256 px | 80 px |
At 288 px the 80 % limit showed part of the next card, a hint that there is more to scroll. Scroll snapping, Baseline
widely available, stops each scroll at the start of a card. Give the scroller tabindex="0", so keyboard users can
focus it and scroll it with the arrow keys, and a role="region" with an aria-label, so screen readers say what it
holds. To stop a scroll gesture from moving the page once the scroller reaches its end, overscroll-behavior-inline: contain works in Chromium and Firefox but has limited availability, because Safari doesn’t support it.
Flexbox or grid for each pattern
Choose by who decides the sizes. When the content does, each item taking the room it needs and each line deciding
for itself, use flexbox: the stack, the wrapping row, the sidebar layout, the switch and the hero. When the layout
does, in columns that line up whatever the content, use grid: the scroller, and the card grids of the intrinsic grids
lesson, whose repeat(auto-fill, minmax(min(16rem, 100%), 1fr)) is the grid version of a wrapping row. The flexbox
patterns lesson measured the difference on a card list: with flex-wrap, cards on a short last line grew to fill it,
while grid kept every card in its column.
Knobs that don’t leak
Knobs are custom properties, and custom properties inherit. That is a trap for nested patterns. If a page sets
--stack-gap: 3rem on an outer stack, a stack inside it that sets nothing inherits the 3rem, and a default written
only as a fallback, var(--stack-gap, 1.5rem), never applies:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A knob that leaks into a nested pattern</title>
<style>
body { margin: 0; font: 16px/1.5 Arial, sans-serif; }
/* A stack whose default lives only in the var() fallback */
.stack { display: flex; flex-direction: column; gap: var(--stack-gap, 1.5rem); }
.stack > * { margin-block: 0; }
.roomy { --stack-gap: 3rem; } /* the outer stack is set to wide gaps */
</style>
</head>
<body>
<div class="stack roomy" id="outer">
<h2>Order summary</h2>
<div class="stack" id="inner"><p>Subtotal</p><p>Delivery</p><p>Total</p></div>
</div>
<script>
// The gap a stack draws: the distance from the end of one child to the start of the next.
function measure() {
const space = (id) => {
const [a, b] = document.getElementById(id).children;
return Math.round(b.getBoundingClientRect().top - a.getBoundingClientRect().bottom);
};
console.log(`outer stack ${space('outer')} px between children · inner stack ${space('inner')} px`);
}
measure();
</script>
</body>
</html> Defaults on every pattern · knob-defaults.css
/* The fix patterns.css uses: every stack sets its own default, so nothing is inherited from an outer stack.
:where() keeps the rule at zero specificity, so .roomy still wins on the outer stack. */
:where(.stack) { --stack-gap: 1.5rem; } A registered knob · no-leak.css
/* Registered knobs don't inherit: each pattern starts from the initial value unless the knob is set on it. The
initial value must not depend on anything else, so it is written in px: 1.5rem would make the rule invalid. */
@property --stack-gap {
syntax: '<length>';
inherits: false;
initial-value: 24px;
} | Setup | Outer stack | Inner stack |
|---|---|---|
| default only as a fallback | 48 px | 48 px, leaked |
:where(.stack) { --stack-gap: 1.5rem; } |
48 px | 24 px |
@property with inherits: false and initial-value: 24px |
48 px | 24 px |
@property with initial-value: 1.5rem |
48 px | 48 px: the rule was ignored |
All three engines agreed. The first fix, which patterns.css uses for every knob, gives each pattern its own default,
so there is nothing left to inherit; :where() has zero specificity, so any class that sets a knob still wins. The
second fix registers the property so it doesn’t inherit at all. It needs an initial value that is computationally
independent, a fixed length such as 24px; 1.5rem depends on the root’s font size, so the rule is invalid and
ignored, as the last row shows. Registered custom properties are Baseline newly available. Give knobs names that say
which pattern they belong to, such as --stack-gap and --sidebar-width, so two patterns never read each other’s.
Composing a page from patterns
Patterns compose because each one only lays out its own children. This landing page nests four of them, a hero with a wrapping row and a stack inside it, a switch of three panels, and a sidebar layout whose content holds a scroller, and the only CSS of its own sets knobs:
landing.css
/* The landing page: four patterns composed, and nothing but knobs set. There is no media query. */
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font: 1rem/1.5 Arial, sans-serif; }
.page { --stack-gap: 3rem; } /* wide gaps between the page's sections only */
.intro { --hero-height: min(100svh, 36rem); } /* a hero that never gets taller than 36rem */
.features { --switch-at: 50rem; padding-inline: 1rem; } /* three panels side by side from 50rem */
.shop { --sidebar-width: 15rem; --content-min: 55%; padding-inline: 1rem; }
.products { --item-width: 14rem; }
.panel, .filters, .product { padding: 1rem; border: 1px solid #767676; border-radius: 8px; } landing.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A landing page from four patterns</title>
<link rel="stylesheet" href="patterns.css">
<link rel="stylesheet" href="landing.css">
</head>
<body>
<div class="page stack">
<header class="intro hero">
<nav class="row-wrap" aria-label="Main"><strong>Kalakriti</strong><a href="#">Sarees</a><a href="#">Kurtas</a><a href="#">Home</a><a href="#">Gifts</a></nav>
<div class="hero-middle stack"><h1>Handloom, straight from the weavers</h1><p>Free delivery on orders above ₹499.</p></div>
<p>Scroll for this week's picks</p>
</header>
<section class="features switch" aria-label="Why shop with us">
<div class="panel">Free delivery</div><div class="panel">Easy returns</div><div class="panel">Cash on delivery</div>
</section>
<div class="shop with-sidebar">
<aside class="filters stack"><h2>Filters</h2><p>Fabric</p><p>Price</p></aside>
<main class="stack">
<h2>This week's picks</h2>
<div class="products scroller" tabindex="0" role="region" aria-label="This week's picks">
<article class="product">Cotton saree</article><article class="product">Silk dupatta</article><article class="product">Linen kurta</article><article class="product">Block-print bedsheet</article><article class="product">Brass diya set</article>
</div>
</main>
</div>
</div>
<script>
const rows = (el) => new Set([...el.children].map((c) => Math.round(c.getBoundingClientRect().top))).size;
const w = (el) => Math.round(el.getBoundingClientRect().width);
const q = (s) => document.querySelector(s);
const products = q('.products');
console.log([
`${innerWidth} px`,
`hero ${Math.round(q('.intro').getBoundingClientRect().height)} px tall`,
`nav ${rows(q('nav'))} line(s)`,
`features ${rows(q('.features'))} row(s)`,
`shop ${rows(q('.shop'))} row(s), filters ${w(q('.filters'))} px, picks ${w(q('main'))} px`,
`product cards ${w(q('.product'))} px, scrolling ${products.scrollWidth - products.clientWidth} px`,
`page ${document.documentElement.scrollWidth} px wide`,
].join(' · '));
</script>
</body>
</html> | Window | Navigation | Feature panels | Filters and picks |
|---|---|---|---|
| 320 px | 2 lines | stacked | filters above, 288 px |
| 480 px | 1 line | stacked | filters above, 448 px |
| 768 px | 1 line | stacked | side by side, 240 and 472 px |
| 1024 px | 1 line | one row | side by side, 241 and 727 px |
| 1280 px | 1 line | one row | side by side, 241 and 983 px |
| 1600 px | 1 line | one row | side by side, 241 and 1303 px |
The three engines produced the same table to the pixel, and the page never scrolled sideways; the product cards stayed 224 px wide inside their scroller. Four nested patterns at six widths came from the 42-line library, six knob settings and no media query. The knobs can be checked with arithmetic before any browser runs:
// What the landing page's knobs do at each width of the content area (the window minus 1rem of padding on each
// side). The sidebar layout keeps the filters beside the picks while 15rem + the 1.5rem gap + 55 % of the width still
// fit on one line; the three feature panels share a row from 50rem.
const REM = 16;
const sidebar = { width: 15 * REM, gap: 1.5 * REM, contentMin: 0.55 };
const switchAt = 50 * REM;
const besideEachOther = (w) => sidebar.width + sidebar.gap + sidebar.contentMin * w <= w;
const wrapsBelow = (sidebar.width + sidebar.gap) / (1 - sidebar.contentMin);
console.log(`The filters drop below the picks under ${wrapsBelow.toFixed(2)} px; the panels share a row from ${switchAt} px.`);
console.log('window area filters panels');
for (const windowWidth of [320, 480, 768, 1024, 1280, 1600]) {
const area = windowWidth - 2 * REM;
const filters = besideEachOther(area) ? 'beside' : 'above';
const panels = area >= switchAt ? 'one row' : 'stacked';
console.log(`${String(windowWidth).padStart(6)} ${String(area).padStart(4)} ${filters.padEnd(7)} ${panels}`);
} Output
The filters drop below the picks under 586.67 px; the panels share a row from 800 px. window area filters panels 320 288 above stacked 480 448 above stacked 768 736 beside stacked 1024 992 beside one row 1280 1248 beside one row 1600 1568 beside one row
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node knobs_at.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 program and the browsers agree: the filters move beside the picks between the 448 px and 736 px content widths, at 586.67 px, and the panels share a row from 800 px. When a page does need a breakpoint, for a navigation menu that turns into a button, for example, keep it to that one component and take its value from the content, as the first lesson of this module did.
CSS Flexbox Generator & Playground Try flex-wrap, flex-grow and flex-basis values on a row of items before you turn them into a pattern.Practise: predict the patterns
Exercise · Medium · JavaScript
Predict what three layout patterns do at any width
Layout patterns are predictable: their knobs and the space they get decide everything. Write three functions in patterns.mjs and export them. All lengths are in px.
- sidebarBeside(width, { sidebar = 256, gap = 24, contentMin = 0.6 } = {}) says whether the sidebar layout keeps its sidebar beside the content in a container width px wide. They share a line while the sidebar's width, the gap and contentMin (a share of the container, 0.6 for 60 %) all fit in the container; otherwise the content wraps below the sidebar. - stackSpace({ display, gap = 0, marginAfter, marginBefore }) returns the space between two neighbouring children of a stack: the first child's bottom margin is marginAfter, the second's top margin marginBefore, both positive. In normal flow (display: 'block') the margins collapse into the larger one and gap does nothing; in a flex or grid container ('flex' or 'grid') nothing collapses, so the gap and both margins add up. - scrollerCard(width, { share = 0.8, max = 256 } = {}) returns the width of each card in a horizontal scroller width px wide whose columns are min(80%, 16rem): the smaller of share × width and max, rounded to two decimals.
The sample tests import the three functions from patterns.mjs and run in your browser.
Starter code · patterns.mjs
/** Whether the sidebar layout keeps its sidebar beside the content in a container `width` px wide. */
export function sidebarBeside(width, { sidebar = 256, gap = 24, contentMin = 0.6 } = {}) {
// Replace this line with your code.
return true;
}
/** The space in px between two neighbouring children of a stack. */
export function stackSpace({ display, gap = 0, marginAfter, marginBefore }) {
// Replace this line with your code.
return 0;
}
/** The width in px of each card of a scroller `width` px wide with columns of min(share, max). */
export function scrollerCard(width, { share = 0.8, max = 256 } = {}) {
// Replace this line with your code.
return max;
} The sample tests · patterns.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { sidebarBeside, stackSpace, scrollerCard } from './patterns.mjs';
test('the default sidebar layout wraps below 700 px', () => {
assert.equal(sidebarBeside(288), false);
assert.equal(sidebarBeside(699), false);
assert.equal(sidebarBeside(700), true);
assert.equal(sidebarBeside(1248), true);
});
test('the landing page\'s knobs move the threshold to 586.67 px', () => {
const knobs = { sidebar: 240, gap: 24, contentMin: 0.55 };
assert.equal(sidebarBeside(448, knobs), false);
assert.equal(sidebarBeside(586, knobs), false);
assert.equal(sidebarBeside(587, knobs), true);
assert.equal(sidebarBeside(736, knobs), true);
});
test('margins collapse in normal flow', () => {
assert.equal(stackSpace({ display: 'block', marginAfter: 16, marginBefore: 16 }), 16);
assert.equal(stackSpace({ display: 'block', gap: 24, marginAfter: 32, marginBefore: 16 }), 32);
});
test('in a flex or grid stack, the gap and both margins add up', () => {
assert.equal(stackSpace({ display: 'flex', gap: 24, marginAfter: 16, marginBefore: 16 }), 56);
assert.equal(stackSpace({ display: 'grid', gap: 24, marginAfter: 0, marginBefore: 0 }), 24);
assert.equal(stackSpace({ display: 'flex', marginAfter: 8, marginBefore: 12 }), 20);
});
test('a scroller card is the smaller of 80 % and 16rem', () => {
assert.equal(scrollerCard(288), 230.4);
assert.equal(scrollerCard(320), 256);
assert.equal(scrollerCard(736), 256);
assert.equal(scrollerCard(250), 200);
});
test('the landing page\'s scroller uses 14rem cards', () => {
assert.equal(scrollerCard(288, { max: 224 }), 224);
assert.equal(scrollerCard(256, { max: 224 }), 204.8);
}); A hint
For the sidebar, compare sidebar + gap + contentMin * width with width. For the stack, Math.max() gives the collapsed margin. For the scroller, Math.min() picks the smaller size, and Math.round(x * 100) / 100 rounds it.
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
5 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 · What is an intrinsic layout, and how is it different from a layout built on breakpoints?
An intrinsic layout responds to the space it actually gets, using wrapping, flexible sizes and limits: flex-wrap,
flex-grow, min(), minmax() and auto-fill. A layout built on breakpoints switches between fixed designs at
chosen viewport widths with media queries. The intrinsic version works wherever it is placed, in a narrow sidebar or a
wide column, because it measures its own container, and it has no gaps between breakpoints. For example, a sidebar
layout with flex-wrap puts the content below the sidebar whenever the content would get narrower than 60 % of the
row, at any window width. Breakpoints are still right for decisions that depend on the page as a whole, such as
turning the main navigation into a menu button.
Key takeaways
- Six patterns cover most layouts: stack, wrapping row, sidebar layout, switch, centred hero and scroller.
- A stack owns its spacing: margins don’t collapse in flex, so remove them and let
gapdecide (24 px, not 56 px). - The sidebar layout wraps when sidebar, gap and the content’s minimum share no longer fit: from 700 px with 16rem and 60 %.
- Use flexbox when the content decides the sizes and grid when the layout does; the scroller is a one-row grid.
- Knobs are custom properties, which inherit: give every pattern its own default with
:where(), or register the knob withinherits: falseand a px initial value. - Composed patterns built the landing page at six widths with no media query, in three engines alike.
References
- CSS Flexible Box Layout Module Level 1: the flex shorthand and flexible lengths (W3C CSS Working Group)
- CSS Flexible Box Layout Module Level 1: line breaking with flex-wrap (W3C CSS Working Group)
- CSS Grid Layout Module Level 2: implicit tracks and grid-auto-flow (W3C CSS Working Group)
- CSS Box Alignment Module Level 3: gaps between boxes (W3C CSS Working Group)
- CSS Box Sizing Module Level 3: intrinsic sizes (W3C CSS Working Group)
- CSS Custom Properties for Cascading Variables Module Level 1: inheritance of custom properties (W3C CSS Working Group)
- CSS Properties and Values API Level 1: the initial-value descriptor (W3C CSS Working Group)
- CSS Scroll Snap Module Level 1 (W3C CSS Working Group)
- Web features explorer: registered custom properties (W3C WebDX Community Group)
- Web features explorer: overscroll-behavior (W3C WebDX Community Group)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress