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

Fluid typography and spacing scales

Build fluid type and spacing scales with clamp(), work out each line from two sizes, keep every step growing with zoom, and test the scale at three widths.

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

What you will learn

  • Build a fluid type scale with clamp() that still respects user zoom
  • Create a matching fluid spacing scale with custom properties
  • Check the scales at small, medium and large widths and at high zoom

Before you start

What you’ll learn · Before you start

What you will learn

  • Build a fluid type scale with clamp() that still respects user zoom
  • Create a matching fluid spacing scale with custom properties
  • Check the scales at small, medium and large widths and at high zoom

Before you start

On this page

Fluid typography lets text grow smoothly with the layout instead of jumping at breakpoints. A size such as clamp(1rem, 0.9583rem + 0.1852vw, 1.125rem) is 16 px up to a 360 px wide layout, 18 px from 1440 px, and on a straight line in between. A scale is a small set of such sizes, one per step from small print to the main heading, plus a matching set of spaces, all kept as custom properties so every component uses the same values. Two decisions make a scale good or bad: where its lines start and end, and whether its text still doubles when someone zooms, which WCAG 2.2 asks through success criterion 1.4.4 (Resize Text). This lesson builds a six-step type scale and an eight-step spacing scale, measures them in Chromium 153, Firefox 155 and WebKit 26.6, and checks every step against zoom.

From two sizes to one clamp()

A fluid size is a line through two points, chosen by design: a size at a narrow layout width and a size at a wide one. For body text of 16 px at 360 px and 18 px at 1440 px:

  • the slope is the growth per px of width, (18 − 16) ÷ (1440 − 360) = 0.001852, which is 0.1852vw, because 1vw is 1 % of the width;
  • the intercept is where the line would meet a 0 px layout, 16 − 0.001852 × 360 = 15.33 px, which is 0.9583rem;
  • clamp() keeps the result between the two sizes, so the line goes flat below 360 px and above 1440 px.

The math functions lesson of this track works through this algebra for one value. A scale needs it for every step, so a small program writes all of them.

A type scale with six steps

A modular scale multiplies each step by the same ratio: with 16 px text and a ratio of 1.2, the steps above are 19.2, 23.04 and 27.65 px. A fluid scale uses two such scales, one for each end. Narrow screens get a small ratio, so a main heading doesn’t fill half the screen with three words; wide screens get a larger one, so headings stand out from the text. This program uses 16 px and 1.2 at 360 px, 18 px and 1.25 at 1440 px, and prints the steps as custom properties, with eight spaces that the next section explains:

A fluid type scale and spacing scale from two ends JavaScript · programs/fluid_scale.mjs
// A fluid type scale and a spacing scale from two ends. At a 360 px viewport the text is 16 px and each type step is
// 1.2 times the one below; at 1440 px the text is 18 px and each step 1.25 times. Spaces are multiples of the text
// size at each end. Every value is a straight line between its two sizes, written as
// clamp(MIN, INTERCEPT + SLOPE vw, MAX) in rem (1rem = 16 px).
const NARROW = { width: 360, text: 16, ratio: 1.2 };
const WIDE = { width: 1440, text: 18, ratio: 1.25 };

const num = (x) => Number(x.toFixed(4));
const rem = (px) => `${num(px / 16)}rem`;

function fluid(minPx, maxPx) {
  const slope = (maxPx - minPx) / (WIDE.width - NARROW.width); // px of size per px of viewport
  const intercept = minPx - slope * NARROW.width; // where the line meets a 0 px viewport
  return `clamp(${rem(minPx)}, ${rem(intercept)} + ${num(slope * 100)}vw, ${rem(maxPx)})`;
}

console.log(':root {');
for (let n = -1; n <= 4; n++) {
  const min = NARROW.text * NARROW.ratio ** n;
  const max = WIDE.text * WIDE.ratio ** n;
  console.log(`  --step-${n}: ${fluid(min, max)}; /* ${num(min)} → ${num(max)} px */`);
}
const SPACES = { '3xs': 0.25, '2xs': 0.5, xs: 0.75, s: 1, m: 1.5, l: 2, xl: 3, '2xl': 4 };
for (const [name, k] of Object.entries(SPACES)) {
  console.log(`  --space-${name}: ${fluid(k * NARROW.text, k * WIDE.text)}; /* ${num(k * NARROW.text)} → ${num(k * WIDE.text)} px */`);
}
// A jump: one value that moves from the s space on phones to the xl space on wide screens.
console.log(`  --space-s-xl: ${fluid(SPACES.s * NARROW.text, SPACES.xl * WIDE.text)}; /* 16 → 54 px */`);
console.log('}');

Output

:root {
  --step--1: clamp(0.8333rem, 0.8111rem + 0.0988vw, 0.9rem); /* 13.3333 → 14.4 px */
  --step-0: clamp(1rem, 0.9583rem + 0.1852vw, 1.125rem); /* 16 → 18 px */
  --step-1: clamp(1.2rem, 1.1312rem + 0.3056vw, 1.4063rem); /* 19.2 → 22.5 px */
  --step-2: clamp(1.44rem, 1.3341rem + 0.4708vw, 1.7578rem); /* 23.04 → 28.125 px */
  --step-3: clamp(1.728rem, 1.5716rem + 0.6952vw, 2.1973rem); /* 27.648 → 35.1563 px */
  --step-4: clamp(2.0736rem, 1.8493rem + 0.997vw, 2.7466rem); /* 33.1776 → 43.9453 px */
  --space-3xs: clamp(0.25rem, 0.2396rem + 0.0463vw, 0.2813rem); /* 4 → 4.5 px */
  --space-2xs: clamp(0.5rem, 0.4792rem + 0.0926vw, 0.5625rem); /* 8 → 9 px */
  --space-xs: clamp(0.75rem, 0.7188rem + 0.1389vw, 0.8438rem); /* 12 → 13.5 px */
  --space-s: clamp(1rem, 0.9583rem + 0.1852vw, 1.125rem); /* 16 → 18 px */
  --space-m: clamp(1.5rem, 1.4375rem + 0.2778vw, 1.6875rem); /* 24 → 27 px */
  --space-l: clamp(2rem, 1.9167rem + 0.3704vw, 2.25rem); /* 32 → 36 px */
  --space-xl: clamp(3rem, 2.875rem + 0.5556vw, 3.375rem); /* 48 → 54 px */
  --space-2xl: clamp(4rem, 3.8333rem + 0.7407vw, 4.5rem); /* 64 → 72 px */
  --space-s-xl: clamp(1rem, 0.2083rem + 3.5185vw, 3.375rem); /* 16 → 54 px */
}

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node fluid_scale.mjs

Its output is the token file, pasted as it was printed:

The scale as custom properties CSS · tokens.css
/* The type and spacing tokens that programs/fluid_scale.mjs prints: 16 px text with type steps of 1.2 at a 360 px
   viewport, 18 px text with steps of 1.25 at 1440 px, spaces as multiples of the text size, and straight lines
   in between. */
:root {
  --step--1: clamp(0.8333rem, 0.8111rem + 0.0988vw, 0.9rem); /* 13.3333 → 14.4 px */
  --step-0: clamp(1rem, 0.9583rem + 0.1852vw, 1.125rem); /* 16 → 18 px */
  --step-1: clamp(1.2rem, 1.1312rem + 0.3056vw, 1.4063rem); /* 19.2 → 22.5 px */
  --step-2: clamp(1.44rem, 1.3341rem + 0.4708vw, 1.7578rem); /* 23.04 → 28.125 px */
  --step-3: clamp(1.728rem, 1.5716rem + 0.6952vw, 2.1973rem); /* 27.648 → 35.1563 px */
  --step-4: clamp(2.0736rem, 1.8493rem + 0.997vw, 2.7466rem); /* 33.1776 → 43.9453 px */
  --space-3xs: clamp(0.25rem, 0.2396rem + 0.0463vw, 0.2813rem); /* 4 → 4.5 px */
  --space-2xs: clamp(0.5rem, 0.4792rem + 0.0926vw, 0.5625rem); /* 8 → 9 px */
  --space-xs: clamp(0.75rem, 0.7188rem + 0.1389vw, 0.8438rem); /* 12 → 13.5 px */
  --space-s: clamp(1rem, 0.9583rem + 0.1852vw, 1.125rem); /* 16 → 18 px */
  --space-m: clamp(1.5rem, 1.4375rem + 0.2778vw, 1.6875rem); /* 24 → 27 px */
  --space-l: clamp(2rem, 1.9167rem + 0.3704vw, 2.25rem); /* 32 → 36 px */
  --space-xl: clamp(3rem, 2.875rem + 0.5556vw, 3.375rem); /* 48 → 54 px */
  --space-2xl: clamp(4rem, 3.8333rem + 0.7407vw, 4.5rem); /* 64 → 72 px */
  --space-s-xl: clamp(1rem, 0.2083rem + 3.5185vw, 3.375rem); /* 16 → 54 px */
}

A page using the steps, with two hero headings for the zoom check further down:

The scale on a page

type-scale.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>A fluid type and spacing scale</title>
  <link rel="stylesheet" href="tokens.css">
  <link rel="stylesheet" href="using-tokens.css">
  <style>
    /* Two hero headings that end at 72 px: one starts at 24 px (a third), one at 36 px (half) */
    .hero { font-size: clamp(1.5rem, 0.5rem + 4.4444vw, 4.5rem); }
    .hero-gentle { font-size: clamp(2.25rem, 1.5rem + 3.3333vw, 4.5rem); }
  </style>
</head>
<body>
  <main class="section stack">
    <h1>Step 4</h1><h2>Step 3</h2><h3>Step 2</h3><h4>Step 1</h4><p>Step 0</p><small>Step -1</small>
    <p class="hero">The steep hero</p>
    <p class="hero-gentle">The gentle hero</p>
  </main>
  <script>
    const px = (el, prop) => parseFloat(getComputedStyle(el)[prop]).toFixed(2);
    const sizes = [...document.querySelectorAll('h1, h2, h3, h4, main > p:not([class]), small, .hero, .hero-gentle')]
      .map((el) => `${el.textContent} ${px(el, 'fontSize')}`);
    const main = document.querySelector('main');
    console.log(`${innerWidth} px: ${sizes.join(' · ')}`);
    console.log(`${innerWidth} px: section padding ${px(main, 'paddingTop')} · gap ${px(main, 'rowGap')}`);
  </script>
</body>
</html>

using-tokens.css

/* The tokens at work: type steps for text, spaces for the gaps between things, and the jump for section padding. */
body { font-size: var(--step-0); line-height: 1.5; }
h1 { font-size: var(--step-4); line-height: 1.1; }
h2 { font-size: var(--step-3); line-height: 1.2; }
h3 { font-size: var(--step-2); line-height: 1.3; }
h4 { font-size: var(--step-1); line-height: 1.4; }
small { font-size: var(--step--1); }

.section {
  padding-block: var(--space-s-xl);   /* 16 px on a phone, 54 px on a wide screen */
  padding-inline: var(--space-s);
}

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-m);                /* the layout owns the space between its children */
}

.card {
  padding: var(--space-s);
  border-radius: var(--space-2xs);
}
Step 320 px 768 px 900 px 1440 px
4, h1 33.18 37.25 38.56 43.95
3, h2 27.65 30.48 31.40 35.16
2, h3 23.04 24.96 25.58 28.12
1, h4 19.20 20.45 20.85 22.50
0, text 16.00 16.76 17.00 18.00
−1, small 13.33 13.74 13.87 14.40

The sizes are Chromium’s and WebKit’s, in px; Firefox was within 0.03 px of them everywhere, and all three matched the program’s line. Two checks are built into the widths. At 320 px every step sits at its minimum, because the line goes flat below 360 px. And 900 px is halfway between the ends, so each size there must be the average of its two ends: step 0 is (16 + 18) ÷ 2 = 17 px, which is what the browsers drew.

Font size against layout width for three fluid sizes, flat below 360 px and above 1440 px, with a dashed line at 720 px for 200 % zoom.Font size against layout width020406080px3607201440layout width in CSS pxa 1440 px windowat 200 % zoomsteep hero, 24 to 72 pxstep 4, 33 to 44 pxstep 0, 16 to 18 pxZoom moves the layout left along each line;every CSS px is then drawn twice as large.

Each fluid size is a line between two widths, and zoom moves along it

Text description of the diagram

The chart plots font size in px, from 0 to 80, against the layout width in CSS px, with marks at 360, 720 and 1440. Each size is flat up to 360 px, rises in a straight line to 1440 px and is flat again after it.

  1. The steep hero, drawn thick and blue, rises from 24 px to 72 px.
  2. Step 4 of the type scale rises from 33 px to 44 px.
  3. Step 0, the body text, rises from 16 px to 18 px, almost flat.

A dashed vertical line at 720 px marks a 1440 px window at 200 % zoom, with a dot where it crosses each line: 40 px for the hero, 36.8 px for step 4 and 16.7 px for step 0. Zoom moves the layout left along each line, and every CSS px is then drawn twice as large, so the steep hero is drawn at 80 px, only a little bigger than its 72 px at 100 %.

Keep the scale small, about six steps of type and eight of space, and give the steps neutral names (--step-2, --space-m) rather than names of elements, so a card title can use step 2 without pretending to be an h3.

Zoom: does every step still double?

Browser zoom does two things at once. The layout gets narrower in CSS px, by the zoom factor, and every CSS px is drawn that many times larger. A fluid size therefore slides down its line while it is drawn bigger: in a 1440 px window at 200 %, the layout is 720 px wide and step 0 is 16.67 px, drawn at 33.3 px, only 1.85 times its 18 px at 100 %. WCAG’s failure F94 describes the extreme case, text in viewport units alone, which never grows at all. This program checks every step, using the zoom levels Chromium offers:

The drawn size of each step as a 1440 px window is zoomed JavaScript · programs/zoom_check.mjs
// Does each step of the type scale still reach twice its size when a 1440 px window is zoomed? At zoom z the layout
// is 1440 / z CSS px wide, so the computed size falls along the line, and every CSS px is drawn z times larger.
// WCAG 2.2 success criterion 1.4.4 asks that text can be resized to 200 %. The zoom levels are Chromium's presets.
const ZOOMS = [1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5];
const WINDOW = 1440;

function line(minPx, maxPx, minWidth = 360, maxWidth = 1440) {
  const slope = (maxPx - minPx) / (maxWidth - minWidth);
  return (width) => Math.min(Math.max(minPx, minPx + slope * (width - minWidth)), maxPx);
}

const SIZES = [];
for (let n = -1; n <= 4; n++) SIZES.push([`step ${n}`, line(16 * 1.2 ** n, 18 * 1.25 ** n)]);
SIZES.push(['steep hero', line(24, 72)]);
SIZES.push(['gentle hero', line(36, 72)]);

const drawn = (size, z) => z * size(WINDOW / z); // in px of the screen at 100 %

console.log('size         100 %   200 %   250 %   500 %   doubles at');
for (const [name, size] of SIZES) {
  const base = drawn(size, 1);
  const first = ZOOMS.find((z) => drawn(size, z) >= 2 * base - 1e-9);
  const cells = [1, 2, 2.5, 5].map((z) => drawn(size, z).toFixed(1).padStart(6)).join('  ');
  console.log(`${name.padEnd(11)}${cells}   ${first ? `${first * 100} %` : 'never, even at 500 %'}`);
}

Output

size         100 %   200 %   250 %   500 %   doubles at
step -1      14.4    27.4    33.9    66.7   250 %
step 0       18.0    33.3    41.0    80.0   250 %
step 1       22.5    40.6    49.6    96.0   250 %
step 2       28.1    49.5    60.1   115.2   250 %
step 3       35.2    60.3    72.9   138.2   250 %
step 4       43.9    73.5    88.3   165.9   250 %
steep hero   72.0    80.0    84.0   120.0   never, even at 500 %
gentle hero  72.0    96.0   108.0   180.0   400 %

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node zoom_check.mjs

The measured pages agreed: laid out at 720, 576 and 288 px wide, the widths of a 1440 px window at 200 %, 250 % and 500 %, the three engines computed the sizes the program used, within 0.03 px. Every step of the scale reached twice its size at 250 %, so readers can enlarge all text to 200 %, as success criterion 1.4.4 asks. The steep hero, 24 px on phones and 72 px on wide screens, never did: at 500 % the layout is 288 px wide, the heading sits at its 24 px minimum, and 5 × 24 = 120 px is short of 2 × 72 = 144 px.

The rule follows from that arithmetic. At the highest zoom the layout is narrower than the line’s start, so text is at its minimum, drawn as many times larger as the zoom. Chromium’s highest desktop zoom is 500 %, so a size doubles by then only if its maximum is at most 2.5 times its minimum. The gentle hero, 36 to 72 px, is twice its minimum and doubled at 400 %. The ratios inside this scale are below 1.4, which is why every step doubled by 250 %.

The reader’s text-size setting is the other way to enlarge text, and it scales only the rem parts. With the default raised from 16 to 20 px, Chromium and Firefox drew step 0 at 21.83 px instead of 18 px in a 1440 px window, 1.21 times larger rather than 1.25, and the steep hero grew only from 72 to 74 px, because nearly all of its size came from vw. The smaller the vw part, the more text follows both zoom and the reader’s setting.

A spacing scale that matches

Spaces belong to the same system as text: padding inside a card, the gap between paragraphs, the margin around a section. The program above makes each space a multiple of the text size at each end: the s space is the text size itself, 16 px at 360 px and 18 px at 1440 px, m is 1.5 times, xl 3 times, from 3xs (a quarter) to 2xl (four times). Eight steps cover a whole interface.

Some spacing should change more than text does. A section’s padding can be the s space on a phone and the xl space on a wide screen, so the token --space-s-xl runs from 16 px to 54 px. In using-tokens.css the section’s padding uses it and the stack’s gaps use --space-m; measured in all three engines, the padding was 16, 30.35, 35 and 54 px at 320, 768, 900 and 1440 px, while the gap moved only from 24 to 27 px. Let layouts own their spacing through gap, as the stack does, so a component dropped into a stack needs no margins of its own.

Inside components, use cqi

vw follows the window, which is right for the page’s own text and wrong for a component that can sit in a sidebar or in a wide column. Inside a component, put cqi in the middle of the clamp(): the container queries lesson sizes a card title with clamp(1.125rem, 0.75rem + 3cqi, 2rem), which measured 20.4 px in a 280 px slot and 31.2 px in a 640 px one on the same page. With no container around it, cqi falls back to the small viewport width, so the same token still works outside a container.

Checking a scale

  1. Three widths. Measure each step at 320, 768 and 1440 px and compare with the numbers the scale was built from; anything more than half a pixel off is a typo in a token.
  2. The middle. At the halfway width, each size is the average of its ends; a different value means the slope or the intercept is wrong.
  3. Zoom. Find the first zoom level that doubles each step in a 1440 px window, as the program does; a step that never doubles needs a larger minimum or a smaller maximum.
  4. The reader’s setting. Raise the browser’s default text size to 20 px and check that every step grows.
  5. The longest words. At 320 px, the main heading must still fit its longest word without overflowing.
CSS Clamp Generator (Fluid Typography) Enter two sizes and two widths to see the line on a graph and check which window widths still reach 200 % zoom. Type Scale Generator Compare modular scales by ratio and see the steps side by side before you choose the two ends.

Practise: a fluid scale that passes zoom

Exercise · Medium · JavaScript

Write fluid clamp() values and check them against zoom

Write three functions in fluid.mjs and export them. Sizes and widths are in px, and 1rem is 16 px.

- fluidClamp(minPx, maxPx, minWidth, maxWidth) returns the CSS for a size that is minPx up to a layout width of minWidth, maxPx from maxWidth, and a straight line in between, as clamp(MINrem, INTERCEPTrem + SLOPEvw, MAXrem). Round every number to four decimals and drop trailing zeros, so 16 to 18 px between 360 and 1440 px is clamp(1rem, 0.9583rem + 0.1852vw, 1.125rem). - spaceScale(narrowText, wideText, steps, minWidth, maxWidth) takes steps as an object such as { s: 1, m: 1.5 } and returns an object with one custom property per step, { '--space-s': '…', '--space-m': '…' }: each step's size is its multiple of narrowText at minWidth and of wideText at maxWidth. - doublesAt(minPx, maxPx, minWidth, maxWidth, windowWidth) returns the first zoom level, as a percentage, at which a window windowWidth px wide draws the text at least twice as large as at 100 %, or null when none does. Try 110, 125, 150, 175, 200, 250, 300, 400 and 500 %. At zoom z the layout is windowWidth / z px wide, and every px is drawn z times larger.

The sample tests import the three functions from fluid.mjs and run in your browser.

Starter code · fluid.mjs

/** clamp(MINrem, INTERCEPTrem + SLOPEvw, MAXrem) for a size from minPx at minWidth to maxPx at maxWidth. */
export function fluidClamp(minPx, maxPx, minWidth, maxWidth) {
  // Replace this line with your code.
  return '';
}

/** One --space-<name> custom property per step: a multiple of the text size at each end. */
export function spaceScale(narrowText, wideText, steps, minWidth, maxWidth) {
  // Replace this line with your code.
  return {};
}

/** The first zoom level, in %, that draws the text at least twice as large, or null. */
export function doublesAt(minPx, maxPx, minWidth, maxWidth, windowWidth) {
  // Replace this line with your code.
  return null;
}
The sample tests · fluid.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { fluidClamp, spaceScale, doublesAt } from './fluid.mjs';

/** The size, in px, that a clamp(Arem, Brem + Cvw, Drem) string gives at a layout `width` px wide. */
function sizeAt(css, width) {
  const m = /^clamp\((-?[\d.]+)rem, (-?[\d.]+)rem \+ (-?[\d.]+)vw, (-?[\d.]+)rem\)$/.exec(css);
  assert.ok(m, `not in the form clamp(Arem, Brem + Cvw, Drem): ${css}`);
  const [min, intercept, slope, max] = m.slice(1).map(Number);
  return Math.min(Math.max(min * 16, intercept * 16 + (slope * width) / 100), max * 16);
}
const near = (actual, expected) => assert.ok(Math.abs(actual - expected) <= 0.5, `expected about ${expected} px, got ${actual}`);

test('body text from 16 to 18 px between 360 and 1440 px', () => {
  assert.equal(fluidClamp(16, 18, 360, 1440), 'clamp(1rem, 0.9583rem + 0.1852vw, 1.125rem)');
});

test('a heading line hits its sizes at both ends and in the middle', () => {
  const css = fluidClamp(28, 56, 375, 1440);
  near(sizeAt(css, 375), 28);
  near(sizeAt(css, 1440), 56);
  near(sizeAt(css, 907.5), 42);
  near(sizeAt(css, 320), 28);
  near(sizeAt(css, 1920), 56);
});

test('a space scale has one custom property per step', () => {
  const scale = spaceScale(16, 18, { s: 1, m: 1.5, xl: 3 }, 360, 1440);
  assert.deepEqual(Object.keys(scale), ['--space-s', '--space-m', '--space-xl']);
  assert.equal(scale['--space-m'], 'clamp(1.5rem, 1.4375rem + 0.2778vw, 1.6875rem)');
  near(sizeAt(scale['--space-xl'], 360), 48);
  near(sizeAt(scale['--space-xl'], 1440), 54);
});

test('a gentle scale doubles at 250 % in a 1440 px window', () => {
  assert.equal(doublesAt(16, 18, 360, 1440, 1440), 250);
  assert.equal(doublesAt(33.18, 43.95, 360, 1440, 1440), 250);
});

test('a size that triples never doubles, one that doubles needs 400 %', () => {
  assert.equal(doublesAt(24, 72, 360, 1440, 1440), null);
  assert.equal(doublesAt(36, 72, 360, 1440, 1440), 400);
});

test('a size that does not change doubles at 200 %', () => {
  assert.equal(doublesAt(16, 16, 360, 1440, 1280), 200);
});
A hint

The slope is (maxPx - minPx) / (maxWidth - minWidth), in px of size per px of width; as vw it is 100 times that. The intercept is minPx - slope * minWidth, in px; divide by 16 for rem. Number(x.toFixed(4)) rounds and drops the trailing zeros. For the zoom, compute the size along the line, clamped, at windowWidth / z, and multiply it by z.

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 Text should be 16 px at a 320 px layout width and 20 px at 1280 px. What is the slope of the line, written in vw?

    Type a number.

    Show the answer to question 1

    Answer: 0.4167 vw (anything from 0.4157 to 0.4177 counts)

    The size grows 4 px over 960 px of width, 0.004167 px per px. A vw is 1 % of the width, so the slope is 100 × 0.004167 = 0.4167vw. The intercept is 16 − 0.004167 × 320 = 14.67 px, or 0.9167rem, which gives clamp(1rem, 0.9167rem + 0.4167vw, 1.25rem).

  2. Question 2 of 5 Which of these font sizes still grows when a reader zooms the page?

    Choose one answer.

    Show the answer to question 2

    Answer: clamp(1rem, 0.9rem + 0.5vw, 1.25rem)

    Zoom draws every CSS px larger and makes the layout narrower by the same factor, so a size made only of viewport units stays the same size on screen. The rem parts don't shrink with the layout, so the first value grows. In the lesson's run, sizes like it reached twice their size at 250 % zoom.

  3. Question 3 of 5 In the lesson's 1440 px window, at which zoom level, in %, did body text that is fluid from 16 to 18 px first reach twice its size?

    Type a number.

    Show the answer to question 3

    Answer: 250 %

    At 200 % the layout is 720 px wide, where the text is 16.67 px, drawn at 33.3 px: 1.85 times its 18 px. At 250 % the layout is 576 px, the text 16.4 px, drawn at 41 px: more than twice. Every step of the lesson's scale doubled at 250 %.

  4. Question 4 of 5 A hero heading is fluid from 24 px at 360 px to 72 px at 1440 px. Why does it never reach twice its size in a 1440 px window, even at 500 % zoom?

    Choose one answer.

    Show the answer to question 4

    Answer: At 500 % it is drawn at most 5 × 24 = 120 px, less than twice its 72 px

    At high zoom the layout is narrower than 360 px, so the heading sits at its 24 px minimum, drawn 5 times larger at 500 %: 120 px, short of 144 px. A maximum of at most 2.5 times the minimum always doubles by 500 %; the gentle hero, 36 to 72 px, doubled at 400 %.

  5. Question 5 of 5 A card's title should grow with the card, and the card appears in a sidebar and in a wide column. Which unit belongs in the middle of its clamp()?

    Choose one answer.

    Show the answer to question 5

    Answer: cqi, a percentage of the card's container

    1cqi is 1 % of the nearest container's inline size, so the title follows the slot the card sits in, not the window. A percentage in font-size is relative to the parent's font size, not to any width.

Interview questions

Fresher · Why should a fluid font size keep rem in its middle value instead of using vw alone?

Viewport units follow the layout’s width in CSS px, and zooming makes that width smaller by exactly the zoom factor, so text sized only in vw is drawn the same size at 100 % and at 400 %. WCAG’s failure F94 describes it: such text can’t be enlarged to 200 % by zooming, and the browser’s text-size setting doesn’t change it either. A rem part, as in clamp(1rem, 0.9583rem + 0.1852vw, 1.125rem), doesn’t shrink with the layout and follows the reader’s text size, so the text still grows: that size reached twice its 100 % size at 250 % zoom in a 1440 px window. Keeping the vw part small and the maximum within 2.5 times the minimum keeps the text able to double at the zoom levels browsers offer.

Key takeaways

  • A fluid size is a line between two (width, size) points: slope in vw, intercept in rem, kept between the ends by clamp().
  • Build a scale from two modular scales, a small ratio for narrow screens and a larger one for wide screens, and keep it to about six steps of type and eight of space, as custom properties.
  • Check each step at 320, 768 and 1440 px, and at the halfway width, where it must be the average of its ends.
  • Zoom narrows the layout while drawing it larger: every step of the lesson’s scale doubled at 250 %, while a hero from 24 to 72 px never did.
  • Keep each maximum within 2.5 times its minimum so the text doubles by 500 % zoom; prefer a small vw part.
  • Inside components use cqi, and let layouts own their spacing with gap.

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.