Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 35 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 35 minutes

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 layout patterns in a grid: vertical stack, wrapping row, sidebar layout, row-to-column switch, centred hero and horizontal scroller.Six patterns, one knob eachVertical stack--stack-gapWrapping row--row-gapSidebar layout--content-minRow-to-column switch--switch-atCentred hero--hero-heightHorizontal scroller--item-widthstacks when the contentwould get too narrowall in one row, orall stacked belowthe thresholdscrolls inside itself,never the page

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.

  1. Vertical stack: three bars one above the other with equal space between them. Knob: --stack-gap.
  2. Wrapping row: rounded chips of different widths on three lines. Knob: --row-gap.
  3. Sidebar layout: a narrow box beside a wide one; it stacks when the content would get too narrow. Knob: --content-min.
  4. Row-to-column switch: three equal boxes in one row; all of them stack below the threshold. Knob: --switch-at.
  5. 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.
  6. 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 patterns, each with its knobs CSS · patterns.css
/* 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:

The six patterns on one page HTML · patterns.html
<!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:

Margins and gap in a stack HTML · stack-margins.html
<!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.

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:

A knob that leaks into a nested stack HTML · nested-knobs.html
<!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>
Two ways to stop the leak

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:

A landing page from four patterns, with no media query

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 JavaScript · programs/knobs_at.mjs
// 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

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.

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.

  1. Question 1 of 5 Two paragraphs with margins of 16 px above and below sit in display: flex; flex-direction: column; gap: 24px. How much space is between them?

    Type a number.

    Show the answer to question 1

    Answer: 56 px

    Margins don't collapse in a flex container, so the first paragraph's 16 px, the 24 px gap and the second paragraph's 16 px add up to 56 px, as all three engines measured. In normal flow the margins would collapse to 16 px. That is why the stack pattern sets its children's block margins to 0 and lets gap own the space.

  2. Question 2 of 5 A sidebar layout has a 240 px sidebar, a 24 px gap and content that must keep at least 55 % of the container. Below what container width does the content wrap under the sidebar?

    Type a number.

    Show the answer to question 2

    Answer: 586.67 px (anything from 586.17 to 587.17 counts)

    The two share a line while 240 + 24 + 0.55 × width ≤ width, that is while 264 ≤ 0.45 × width, so from 264 ÷ 0.45 = 586.67 px. On the lesson's landing page the filters sat beside the picks at a 736 px container and above them at 448 px.

  3. Question 3 of 5 An outer stack sets --stack-gap: 3rem, and a stack nested inside it, which sets nothing, also gets 48 px gaps. What fixes it?

    Choose one answer.

    Show the answer to question 3

    Answer: Giving every stack its own default, :where(.stack) { --stack-gap: 1.5rem; }

    Custom properties inherit, and a fallback is only used when nothing was inherited, so the inner stack took the outer 3rem. A default set on every stack wins over inheritance, and :where() keeps it at zero specificity so any class can still change it. In the lesson's run the inner gap went from 48 to 24 px in all three engines.

  4. Question 4 of 5 A stylesheet registers @property --stack-gap { syntax: '<length>'; inherits: false; initial-value: 1.5rem; }. What does the browser do with it?

    Choose one answer.

    Show the answer to question 4

    Answer: It ignores the rule, because 1.5rem depends on the root's font size and an initial value must not

    An initial value must be computationally independent, a value such as 24px that needs nothing else to compute. With 1.5rem all three engines ignored the rule, and the nested stack kept inheriting; with 24px they registered it and the nested stack got its own 24 px gap.

  5. Question 5 of 5 A horizontal scroller of product cards has no links or buttons inside. What lets a keyboard user reach it and scroll it?

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • A role="region" and an aria-label that names it
    • tabindex="0" on the scroller

    With tabindex="0" the scroller can take focus, and the arrow keys then scroll it; the role and the label tell screen reader users what they are in. Scroll snapping changes where scrolling stops, not whether it can start, and hiding the page's overflow would hide content.

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 gap decide (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 with inherits: false and a px initial value.
  • Composed patterns built the landing page at six widths with no media query, in three engines alike.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.