CSS Module 12 – Responsive design: media and container queries
Responsive design foundations
Set up the viewport meta tag, write mobile-first CSS with breakpoints taken from the content, and test reflow at 320 px, zoom and text spacing.
What you will learn
- Set up the viewport meta tag and a mobile-first stylesheet
- Design fluid layouts that meet WCAG reflow at 320 CSS px
- Test layouts across widths, zoom levels and the WCAG text-spacing override
Before you start
What you’ll learn · Before you start
What you will learn
- Set up the viewport meta tag and a mobile-first stylesheet
- Design fluid layouts that meet WCAG reflow at 320 CSS px
- Test layouts across widths, zoom levels and the WCAG text-spacing override
Before you start
On this page
A responsive page is one HTML document and one stylesheet that work at every width people use: a 320 px phone, a
tablet turned on its side, a laptop zoomed to 200 %, a wide monitor. It rests on three habits. The viewport meta
tag tells phones to lay the page out at their own width instead of a desktop’s. The stylesheet is written mobile
first: flexible boxes that need no query at all, then min-width queries that add columns once the content has
room for them. And the page is tested the way people read it: in narrow windows, zoomed in, and with their own
text spacing, which WCAG 2.2 checks at level AA through success criteria 1.4.10 (Reflow) and 1.4.12 (Text
Spacing). This lesson measures each step in Chromium 153, Firefox 155 and WebKit 26.6, with pages you can open in
your own browser.
The viewport meta tag
Phones arrived when most pages were designed for desktop screens. So unless a page says otherwise, a mobile browser
lays it out as if the screen were as wide as a small desktop window, then shrinks the result to fit. One line in
the head turns that off. The two pages below differ only in that line, and each prints what the browser did:
With the tag · viewport-set.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A page with the viewport tag</title>
<style>
body { margin: 0; padding: 1rem; font: 16px/1.5 Arial, sans-serif; }
.note { padding: 1rem; background: #fff3c4; }
@media (width <= 40rem) { .note { background: #c8f2d4; } }
</style>
</head>
<body>
<p class="note">This paragraph turns green when the layout is at most 40rem (640 px) wide.</p>
<script>
addEventListener('load', () => requestAnimationFrame(() => {
const scale = window.visualViewport ? visualViewport.scale : 1; // how much the browser shrank the layout to fit
console.log(`layout ${document.documentElement.clientWidth} px wide · (width <= 40rem) ${matchMedia('(width <= 40rem)').matches} · shown at ${Math.round(scale * 100)} % · 16 px text looks ${(16 * scale).toFixed(1)} px`);
}));
</script>
</body>
</html> Without the tag · viewport-missing.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>A page without the viewport tag</title>
<style>
body { margin: 0; padding: 1rem; font: 16px/1.5 Arial, sans-serif; }
.note { padding: 1rem; background: #fff3c4; }
@media (width <= 40rem) { .note { background: #c8f2d4; } }
</style>
</head>
<body>
<p class="note">This paragraph turns green when the layout is at most 40rem (640 px) wide.</p>
<script>
addEventListener('load', () => requestAnimationFrame(() => {
const scale = window.visualViewport ? visualViewport.scale : 1; // how much the browser shrank the layout to fit
console.log(`layout ${document.documentElement.clientWidth} px wide · (width <= 40rem) ${matchMedia('(width <= 40rem)').matches} · shown at ${Math.round(scale * 100)} % · 16 px text looks ${(16 * scale).toFixed(1)} px`);
}));
</script>
</body>
</html> On a phone 360 CSS px wide, emulated in Chromium and WebKit:
| With the tag | Without the tag | |
|---|---|---|
| layout width | 360 px | 980 px |
(width <= 40rem) |
matches | does not match |
| page shown at | 100 % | 37 % |
| 16 px text looks | 16 px | 5.9 px |
Both engines gave the same numbers. Without the tag, the page was laid out 980 px wide and drawn at 37 % of its size, so the query written for narrow screens never matched and the reader had to zoom in to read anything. Firefox’s desktop engine, which Playwright drives, laid out both pages 360 px wide, because desktop browsers ignore the tag.
width=device-width makes the layout viewport as wide as the screen in CSS px, which also sets what 100vw means.
initial-scale=1 starts the page at 100 % zoom, so a browser doesn’t zoom out when something overflows. Leave out
maximum-scale=1 and user-scalable=no: they ask the browser to stop pinch zoom, which people with low vision depend
on, and WCAG’s success criterion 1.4.4 expects text to resize to 200 %. Safari on iOS ignores them, but other
browsers may not.
Mobile first: start with one column
A mobile-first stylesheet starts with the layout for the narrowest screen, then lets each min-width query add
something once the content fits. A desktop-first stylesheet starts with the widest layout and takes things away. The
two files below give a grid of product cards the same layout at every width (all three engines placed the same
columns at 320, 527, 528, 783 and 784 px):
Mobile first · mobile-first.css
/* Mobile first: the base styles are the one-column layout, and each query adds a column once the cards fit. The
breakpoints come from the content (programs/columns_fit.mjs): a card needs 15rem, columns are 1rem apart and the
page has 1rem of padding on each side. */
.page {
padding-inline: 1rem;
}
.products {
display: grid;
gap: 1rem;
}
@media (width >= 33rem) {
.products { grid-template-columns: repeat(2, 1fr); }
}
@media (width >= 49rem) {
.products { grid-template-columns: repeat(3, 1fr); }
} Desktop first · desktop-first.css
/* Desktop first: the same layout, written from the widest screen down. The base styles are three columns, and each
query takes one away again. */
.page {
padding-inline: 1rem;
}
.products {
display: grid;
gap: 1rem;
grid-template-columns: repeat(3, 1fr);
}
@media (width < 49rem) {
.products { grid-template-columns: repeat(2, 1fr); }
}
@media (width < 33rem) {
.products { grid-template-columns: 1fr; }
} The mobile-first version is the safer one, for three reasons.
- The narrow layout needs the least code. Block boxes already stack and fill the width in normal flow, so one column needs almost nothing, and each query only adds. The desktop-first file sets three columns and then undoes them twice.
- What a browser shows when it skips a query. Browsers older than Chrome 104, Firefox 102 and Safari 16.4 don’t
understand range syntax such as
(width >= 33rem), so they skip those queries and keep the rest. The mobile-first file then falls back to one long column, which works at any width; the desktop-first file falls back to three columns, under 100 px each on a phone. - Zoom is a narrow screen. Zooming in makes every CSS pixel bigger, so the layout gets fewer of them. This program applies the stylesheet’s two breakpoints to some common windows:
// Which layout the mobile-first stylesheet of this lesson shows. Zooming divides the width of the layout viewport:
// a window 1366 CSS px wide at 200 % zoom lays the page out 683 px wide, as a narrower screen would. The breakpoints
// are the stylesheet's min-width queries, 33rem and 49rem: 528 and 784 px at the default text size of 16 px.
const BREAKPOINTS = [
{ minRem: 33, layout: 'two columns' },
{ minRem: 49, layout: 'three columns' },
];
function layoutAt(cssWidth, textPx = 16) {
let layout = 'one column'; // the base styles, written for the narrowest screens
for (const b of BREAKPOINTS) if (cssWidth >= b.minRem * textPx) layout = b.layout;
return layout;
}
// [what, window width at 100 % zoom in CSS px, zoom %]
const SCREENS = [
['phone', 360, 100],
['tablet', 768, 100],
['laptop', 1366, 100],
['laptop', 1366, 150],
['laptop', 1366, 200],
['desktop', 1920, 300],
['desktop', 1280, 400],
];
console.log('screen window zoom layout width layout');
for (const [what, windowPx, zoom] of SCREENS) {
const width = windowPx / (zoom / 100);
const shown = Number.isInteger(width) ? String(width) : width.toFixed(2);
console.log(`${what.padEnd(9)}${String(windowPx).padStart(6)} ${String(zoom).padStart(4)} % ${shown.padStart(9)} px ${layoutAt(width)}`);
} Output
screen window zoom layout width layout phone 360 100 % 360 px one column tablet 768 100 % 768 px two columns laptop 1366 100 % 1366 px three columns laptop 1366 150 % 910.67 px three columns laptop 1366 200 % 683 px two columns desktop 1920 300 % 640 px two columns desktop 1280 400 % 320 px one column
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node layout_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
A laptop at 200 % gets the two-column layout, and a desktop at 400 % gets the phone’s one column. People who zoom see the layouts you wrote for small screens, so those layouts have to be complete, never a cut-down version of the page.
Every screen comes down to one layout width
Text description of the diagram
Four kinds of screen point to the same box, the layout width in CSS px:
- A phone with the viewport tag: 360 CSS px.
- A phone without the tag: the page is laid out 980 px wide and shown at 37 % of its size.
- A laptop window 1366 px wide at 200 % zoom: 683 CSS px.
- A desktop window 1280 px wide at 400 % zoom: 320 CSS px.
The layout width is compared with the stylesheet's queries, (width >= 33rem) and (width >= 49rem), and they choose one, two or three columns. A zoomed desktop window and a phone get the same layout when their layout widths are the same.
Fluid layouts: nothing wider than the screen
Between breakpoints, a layout has to stretch and shrink on its own. Four habits keep every box inside a narrow screen:
- Widths as limits.
max-inline-size: 60remlets a box shrink below 60rem;width: 960pxdoesn’t. - Images that shrink.
max-inline-size: 100%withblock-size: autoscales an image down to its container and keeps its shape. - Text that wraps. An email address or a long code has no place to break, so it needs
overflow-wrap: anywhere. - Wide content in its own scroller. A table with many columns can scroll sideways inside a box with
overflow-x: auto, while the page around it doesn’t.
Here is a page that breaks all four habits, and a small audit script that lists what makes a page scroll sideways. Run the script in the DevTools console with the window 320 CSS px wide, or load it at the end of a page, as here:
Fixed-width page · legacy.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A fixed-width page</title>
<style>
body { margin: 0; font: 16px/1.5 Arial, sans-serif; }
.page { width: 960px; margin: 0 auto; }
.menu { display: flex; gap: 24px; padding: 0; list-style: none; white-space: nowrap; }
.banner { display: block; }
table { border-collapse: collapse; }
th, td { padding: 8px 16px; border: 1px solid #767676; text-align: start; }
</style>
</head>
<body>
<div class="page">
<ul class="menu"><li><a href="#">Home</a></li><li><a href="#">New arrivals</a></li><li><a href="#">Men</a></li><li><a href="#">Women</a></li><li><a href="#">Kids</a></li><li><a href="#">Home and living</a></li><li><a href="#">Help</a></li></ul>
<img class="banner" width="1200" height="400" alt="Festive sale: up to 40 % off"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 400'%3E%3Crect width='1200' height='400' fill='%23f4d35e'/%3E%3C/svg%3E">
<h1>Your orders</h1>
<p class="help">Questions? Write to customer.support.international.orders@shop.example</p>
<table>
<tr><th>Order</th><th>Placed</th><th>Items</th><th>Total</th><th>Status</th><th>Invoice</th></tr>
<tr><td>#40211</td><td>Yesterday</td><td>3</td><td>₹2,499</td><td>Out for delivery</td><td><a href="#">Download</a></td></tr>
</table>
</div>
<script src="reflow-audit.js"></script>
</body>
</html> reflow-audit.js
// A reflow check for WCAG 2.2 success criterion 1.4.10: run it with the window 320 CSS px wide (or 1280 px at
// 400 % zoom), in the DevTools console or at the end of a page. It lists what makes the page scroll sideways: boxes
// that stick out of their container past the edge of the viewport, and text that cannot wrap to fit its box. Content
// in a scroll container of its own, such as a wide data table in a box with overflow-x: auto, is left out: content
// that needs two dimensions to make sense may scroll both ways.
function reflowAudit() {
const limit = document.documentElement.clientWidth;
const pageWidth = document.documentElement.scrollWidth;
const scrolls = (el) => !['visible', 'clip'].includes(getComputedStyle(el).overflowX);
const insideScroller = (el) => {
for (let a = el.parentElement; a && a !== document.body; a = a.parentElement) if (scrolls(a)) return true;
return false;
};
// A block with its own text: inline boxes such as links have no width of their own to compare with.
const ownText = (el) => getComputedStyle(el).display !== 'inline' && [...el.childNodes].some((n) => n.nodeType === Node.TEXT_NODE && n.textContent.trim());
const name = (el) => el.tagName.toLowerCase() + (el.classList.length ? '.' + el.classList[0] : '');
const problems = [];
for (const el of document.body.querySelectorAll('*')) {
if (el.tagName === 'SCRIPT' || insideScroller(el)) continue;
const box = el.getBoundingClientRect();
const parent = el.parentElement.getBoundingClientRect();
if (box.right > limit + 0.5 && box.right > parent.right + 0.5) {
problems.push(`${name(el)} sticks out of its container and ends at ${Math.round(box.right)} px`);
} else if (ownText(el) && el.scrollWidth > el.clientWidth + 1 && box.left + el.scrollWidth > limit + 0.5) {
problems.push(`${name(el)} holds text that cannot wrap: ${el.scrollWidth} px in a ${el.clientWidth} px box`);
}
}
console.log(`viewport ${limit} px, page ${pageWidth} px wide${pageWidth > limit ? ': it scrolls sideways' : ''}`);
for (const p of problems) console.log(` ${p}`);
return problems.length;
}
reflowAudit(); The audit at 320 px, in all three engines:
| Page | What the audit listed |
|---|---|
| fixed-width page | the .page wrapper ending at 960 px, the banner at 1200 px |
the wrapper fixed with width: auto; max-inline-size: 60rem; padding-inline: 1rem |
three menu items ending at 360, 499 and 555 px, the banner at 1216 px, the email address (380 px of text in a 288 px box), the table at 554 px |
| reflowed page | nothing: the page is 320 px wide |
The second row is the usual experience: the outer box hides the problems inside it, and each fix uncovers the next. The banner moved to 1216 px because of the new 16 px of padding before it. All three engines listed the same boxes at the same pixels.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The same page, reflowed</title>
<style>
body { margin: 0; font: 16px/1.5 Arial, sans-serif; }
.page { max-inline-size: 60rem; margin-inline: auto; padding-inline: 1rem; }
.menu { display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; padding: 0; list-style: none; }
.banner { display: block; max-inline-size: 100%; block-size: auto; }
p { max-inline-size: 65ch; }
.help { overflow-wrap: anywhere; }
.table-scroll { overflow-x: auto; }
table { border-collapse: collapse; }
th, td { padding: 8px 16px; border: 1px solid #767676; text-align: start; white-space: nowrap; }
</style>
</head>
<body>
<div class="page">
<ul class="menu"><li><a href="#">Home</a></li><li><a href="#">New arrivals</a></li><li><a href="#">Men</a></li><li><a href="#">Women</a></li><li><a href="#">Kids</a></li><li><a href="#">Home and living</a></li><li><a href="#">Help</a></li></ul>
<img class="banner" width="1200" height="400" alt="Festive sale: up to 40 % off"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 400'%3E%3Crect width='1200' height='400' fill='%23f4d35e'/%3E%3C/svg%3E">
<h1>Your orders</h1>
<p class="help">Questions? Write to customer.support.international.orders@shop.example</p>
<div class="table-scroll" role="region" aria-label="Your orders" tabindex="0">
<table>
<tr><th>Order</th><th>Placed</th><th>Items</th><th>Total</th><th>Status</th><th>Invoice</th></tr>
<tr><td>#40211</td><td>Yesterday</td><td>3</td><td>₹2,499</td><td>Out for delivery</td><td><a href="#">Download</a></td></tr>
</table>
</div>
</div>
<script src="reflow-audit.js"></script>
</body>
</html> The fixed page keeps the menu on several lines with flex-wrap, scales the banner, lets the address break, limits
paragraphs to 65ch so lines stay readable on a 1440 px screen, and moves the table into a scroll region. The
region has tabindex="0", so keyboard users can reach it and scroll it with the arrow keys, and a role and an
aria-label, so screen readers announce what it holds.
Breakpoints come from the content
Device widths make poor breakpoints: there are hundreds of them, they change every year, and windows, split screens and zoom produce every width in between. Take the breakpoints from the content instead. Widen a narrow window slowly, and add a breakpoint where the layout starts to look wrong or where it has room for more. For a grid of cards, that width can be worked out:
// Breakpoints from the content: from what width does a grid of product cards have room for one more column? A card
// needs at least 15rem to show its photo, name and price; columns are 1rem apart and the page has 1rem of padding on
// each side. n columns first fit at n × card + (n − 1) × gap + 2 × padding.
const REM = 16; // px, the default text size
const card = 15 * REM;
const gap = 1 * REM;
const padding = 1 * REM;
for (let n = 1; n <= 4; n++) {
const px = n * card + (n - 1) * gap + 2 * padding;
console.log(`${n} column${n === 1 ? ' fits' : 's fit'} from ${px} px = ${px / REM}rem`);
} Output
1 column fits from 272 px = 17rem 2 columns fit from 528 px = 33rem 3 columns fit from 784 px = 49rem 4 columns fit from 1040 px = 65rem
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node columns_fit.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
These cards need 15rem each, so two columns fit from 33rem (528 px) and three from 49rem (784 px), the two queries of
the mobile-first file. Had the second column waited for 768 px, the md breakpoint of Bootstrap 5.3 and Tailwind CSS
4, a 600 px window would have shown one column of cards 568 px wide, more than twice what a card needs. Write breakpoints in
rem, so they move with the reader’s default text size: the media queries lesson next in this module shows how
browsers resolve them. A grid with repeat(auto-fill, minmax(15rem, 1fr)) finds the same widths with no query at all,
as the intrinsic grids lesson shows, and the last lesson of this module builds whole layouts that way.
Test the way people read
Responsive testing has four parts, and WCAG 2.2 sets the numbers for three of them.
- Width. Success criterion 1.4.10 (Reflow) asks that content work at 320 CSS px wide without scrolling in two directions, which is a 1280 px window at 400 % zoom. Content that reads sideways, such as vertical text, gets the same rule at 256 CSS px tall. Parts that need two dimensions to make sense, such as data tables, maps, diagrams, video and games, may scroll both ways, but the text around them must still reflow.
- Zoom. Zoom a desktop browser to 200 % and 400 %. Success criterion 1.4.4 (Resize Text) asks that text can grow to 200 % without anything being lost, and zooming, as the program above showed, also moves the page through its narrower layouts.
- Text spacing. Success criterion 1.4.12 asks that nothing is lost when a reader sets line height to 1.5 times the font size, the space after paragraphs to 2 times, letter spacing to 0.12 times and word spacing to 0.16 times. Pages don’t have to use those values, only survive them.
- Real input. Touch, a keyboard and a screen reader on at least one real phone, because emulation shows sizes but not how a page feels under a thumb.
The text-spacing test is a stylesheet. Add it as a user stylesheet, paste it into DevTools or load it with a bookmarklet:
/* The WCAG 2.2 text-spacing test (success criterion 1.4.12). Add it as a user stylesheet, paste it into DevTools or
load it with a bookmarklet, then check that no text is cut off and none overlaps other content. */
* {
line-height: 1.5 !important;
letter-spacing: 0.12em !important;
word-spacing: 0.16em !important;
}
p {
margin-block-end: 2em !important;
} Components with fixed sizes are what it breaks. This page holds a card, a button and a tab twice: first with sizes fixed to fit the text as designed, then with the same sizes as minimums. It prints where text reaches past the edge of a box that hides its overflow:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fixed sizes and the text-spacing test</title>
<style>
body { margin: 0; padding: 1rem; font: 16px/1.25 Arial, sans-serif; }
.card { padding: 12px; border: 1px solid #767676; margin-block-end: 1rem; }
.card h2 { margin: 0 0 8px; font-size: 1.25rem; }
.card p { margin: 0; }
.button { display: inline-block; padding-inline: 16px; border: 2px solid #1d4ed8; color: #1d4ed8; }
.tab { display: inline-block; text-align: center; border-block-end: 3px solid #1d4ed8; }
/* Sizes fixed to fit the text as it was designed */
.fixed .card { height: 76px; overflow: hidden; }
.fixed .button { width: 160px; height: 36px; line-height: 36px; white-space: nowrap; overflow: hidden; }
.fixed .tab { width: 96px; white-space: nowrap; overflow: hidden; }
/* The same components with minimums: they grow with their text */
.flexible .card { min-block-size: 76px; }
.flexible .button { min-inline-size: 160px; min-block-size: 36px; padding-block: 6px; }
.flexible .tab { min-inline-size: 96px; padding-inline: 8px; }
</style>
</head>
<body>
<section class="fixed">
<div class="card"><h2>Free delivery</h2><p>On orders above ₹499 to most pin codes in India, with returns within seven days.</p></div>
<a class="button" href="#">Shop the festive sale</a>
<span class="tab">Bestsellers</span>
</section>
<section class="flexible">
<div class="card"><h2>Free delivery</h2><p>On orders above ₹499 to most pin codes in India, with returns within seven days.</p></div>
<a class="button" href="#">Shop the festive sale</a>
<span class="tab">Bestsellers</span>
</section>
<script>
// Text is cut off when it reaches past the edge of a box that hides its overflow.
function measure() {
for (const section of document.querySelectorAll('section')) {
const report = [...section.querySelectorAll('.card, .button, .tab')].map((el) => {
const range = document.createRange();
range.selectNodeContents(el);
const text = range.getBoundingClientRect();
const box = el.getBoundingClientRect();
const css = getComputedStyle(el);
const below = Math.round(text.bottom - (box.bottom - parseFloat(css.borderBottomWidth)));
const beside = Math.round(text.right - (box.right - parseFloat(css.borderRightWidth)));
if (below > 0) return `${el.className}: text ${below} px past the bottom edge`;
if (beside > 0) return `${el.className}: text ${beside} px past the side`;
return `${el.className} fits, ${Math.round(box.width)} × ${Math.round(box.height)} px`;
});
console.log(`${section.className}: ${report.join(' · ')}`);
}
}
measure();
</script>
</body>
</html> At 360 px wide, before and after the test stylesheet:
| Component | Fixed sizes, before | Fixed sizes, after | Minimums, after |
|---|---|---|---|
card, height: 76px |
fits | text 22 px past the bottom edge | fits, now 168 px tall |
button, width: 160px |
fits | text 21 px past the side | fits, now 233 px wide |
tab, width: 96px |
fits | text 3 px past the side | fits, now 115 px wide |
All three engines measured the same pixels. Wider letters and taller lines need room, so the fix is the same in every row: give components minimum sizes and padding, and let their content decide the rest.
Practise: reflow and spacing checks
Exercise · Easy · JavaScript
Write the checks behind a reflow and text-spacing test
A reflow test needs three numbers: how wide the layout is at a zoom level, which boxes stick out past the screen, and how much spacing the WCAG text-spacing test adds. Write three functions in checks.mjs and export them.
- layoutWidth(windowWidth, zoom) returns the width of the layout viewport, in CSS px, of a window that is windowWidth CSS px wide at 100 % and zoomed to zoom per cent, rounded to two decimals. A window 1280 px wide at 400 % lays the page out 320 px wide. - spacingOverride(fontSize) returns the four values of WCAG 2.2 success criterion 1.4.12 for text of fontSize px, as { lineHeight, paragraphSpacing, letterSpacing, wordSpacing } in px, each rounded to two decimals: 1.5, 2, 0.12 and 0.16 times the font size. - reflowProblems(boxes, viewportWidth) takes boxes as { name, right, inScroller }: where each box ends, in px from the left edge of the viewport, and whether it sits inside a scroll container of its own. It returns the names of the boxes that end more than half a pixel past viewportWidth, in the order given, leaving out the boxes inside a scroll container: content such as a data table may scroll both ways in a box of its own.
The sample tests import the three functions from checks.mjs and run in your browser.
Starter code · checks.mjs
/** The width of the layout viewport, in CSS px, of a window `windowWidth` px wide zoomed to `zoom` %. */
export function layoutWidth(windowWidth, zoom) {
// Replace this line with your code.
return windowWidth;
}
/** The WCAG 2.2 text-spacing values, in px, for text of `fontSize` px. */
export function spacingOverride(fontSize) {
// Replace this line with your code.
return { lineHeight: 0, paragraphSpacing: 0, letterSpacing: 0, wordSpacing: 0 };
}
/** The names of the boxes that stick out past the viewport, leaving out those inside a scroll container. */
export function reflowProblems(boxes, viewportWidth) {
// Replace this line with your code.
return [];
} The sample tests · checks.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { layoutWidth, spacingOverride, reflowProblems } from './checks.mjs';
test('zoom divides the width of the layout', () => {
assert.equal(layoutWidth(1280, 400), 320);
assert.equal(layoutWidth(1366, 200), 683);
assert.equal(layoutWidth(360, 100), 360);
});
test('the layout width is rounded to two decimals', () => {
assert.equal(layoutWidth(1366, 150), 910.67);
assert.equal(layoutWidth(1536, 300), 512);
assert.equal(layoutWidth(1000, 300), 333.33);
});
test('the four text-spacing values for 16 px text', () => {
assert.deepEqual(spacingOverride(16), { lineHeight: 24, paragraphSpacing: 32, letterSpacing: 1.92, wordSpacing: 2.56 });
});
test('the text-spacing values follow the font size', () => {
assert.deepEqual(spacingOverride(18), { lineHeight: 27, paragraphSpacing: 36, letterSpacing: 2.16, wordSpacing: 2.88 });
assert.deepEqual(spacingOverride(15), { lineHeight: 22.5, paragraphSpacing: 30, letterSpacing: 1.8, wordSpacing: 2.4 });
});
test('boxes that end past the viewport are problems, in order', () => {
const boxes = [
{ name: 'header', right: 320, inScroller: false },
{ name: 'banner', right: 1216, inScroller: false },
{ name: 'menu', right: 555, inScroller: false },
{ name: 'footer', right: 300, inScroller: false },
];
assert.deepEqual(reflowProblems(boxes, 320), ['banner', 'menu']);
});
test('half a pixel of rounding is not a problem', () => {
assert.deepEqual(reflowProblems([{ name: 'card', right: 320.5, inScroller: false }], 320), []);
assert.deepEqual(reflowProblems([{ name: 'card', right: 320.75, inScroller: false }], 320), ['card']);
});
test('content in a scroll container of its own is left out', () => {
const boxes = [
{ name: 'table', right: 554, inScroller: true },
{ name: 'email', right: 412, inScroller: false },
];
assert.deepEqual(reflowProblems(boxes, 320), ['email']);
assert.deepEqual(reflowProblems([], 320), []);
}); A hint
At 400 % every CSS pixel takes four times the space, so the layout gets a quarter of the window: divide by zoom / 100. Round with Math.round(value * 100) / 100. For the boxes, filter keeps the ones that end after viewportWidth + 0.5 and are not in a scroller, and map turns them into their names.
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 does the viewport meta tag do, and what happens on a phone without it?
<meta name="viewport" content="width=device-width, initial-scale=1"> tells a mobile browser to make the layout
viewport as wide as the screen in CSS px and to start at 100 % zoom. Without it, mobile browsers assume the page was
designed for desktops: they lay it out about 980 px wide and shrink the result to fit, so text is drawn tiny and the
reader has to pinch to zoom. Media queries written for narrow screens never match, because the layout is 980 px
wide; on a 360 px phone the page is shown at about 37 %. Desktop browsers ignore the tag. Don’t add
user-scalable=no or maximum-scale=1, which try to stop people from zooming.
Key takeaways
- Put
<meta name="viewport" content="width=device-width, initial-scale=1">in every page; without it, a 360 px phone laid the page out 980 px wide and showed it at 37 %. - Write mobile first: one column needs almost no CSS, and
min-widthorwidth >=queries add columns when they fit. - A zoomed window is a narrow screen: 1280 px at 400 % is the 320 px of WCAG’s reflow criterion.
- Use limits instead of fixed widths, shrinking images,
overflow-wrap: anywherefor long words, and scroll regions for tables. - Take breakpoints from the content, in rem: where the layout breaks or has room for more, not from a list of devices.
- Test at 320 px, at 200 % and 400 % zoom, and with the WCAG text-spacing stylesheet; give components minimum sizes so they grow with their text.
References
- Web Content Accessibility Guidelines (WCAG) 2.2: Reflow (W3C)
- Web Content Accessibility Guidelines (WCAG) 2.2: Text Spacing (W3C)
- Web Content Accessibility Guidelines (WCAG) 2.2: Resize Text (W3C)
- Understanding WCAG 2.2: Reflow (W3C)
- Understanding WCAG 2.2: Text Spacing (W3C)
- CSS Viewport Module Level 1 (W3C CSS Working Group)
- <meta name="viewport"> (MDN Web Docs) (Mozilla)
- Media Queries Level 4: the width feature and the range syntax (W3C CSS Working Group)
- CSS Text Module Level 3: overflow-wrap (W3C CSS Working Group)
- Bootstrap 5.3: breakpoints (Bootstrap)
- Tailwind CSS: responsive design (Tailwind Labs)
- Web features explorer: media query range syntax (W3C WebDX Community Group)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress