Skip to content
academia.sh

Lesson 07 / 23

The Principle of Responsive Design

The three components of responsiveness, deriving breakpoints from content by computation, thresholds that resolve on their own through intrinsic sizing, and how a track definition shifts a threshold.

Contents

The multi-column layout lesson made a decision silently: when a column’s width no longer fit its container, the column count was lowered. The threshold did not come from a list of screen widths, it came from the narrowest measure at which the text stayed readable.

This lesson carries the same reasoning to the whole page. At what width a layout needs to change is a computable question; the answer lies not in device names but in the content’s own measurements.

The Three Components of Responsiveness

Responsive design is not the name of a single declaration; it is three behaviors present together.

A fluid grid. The layout’s dimensions are given not with fixed lengths but with ratios and remaining space. fr, percentages, minmax(), and auto do this job; fixed pixel widths are kept only where something genuinely needs to be fixed.

Flexible media. Images and embedded content never exceed their container; their size comes not from their own natural dimensions but from what the container leaves.

Conditional style. When the two above are not enough on their own — when the layout’s structure has to change — style is bound to a condition.

The order matters. The more work the first two components do, the less is left for the third. Every condition written with a query is a branch that has to be maintained in the stylesheet; every threshold resolved through intrinsic sizing never appears in the file at all.

A Breakpoint Is a Measurement

A breakpoint is the width at which a layout’s structure changes. The question asked correctly is this: how narrow can this component get before it loses its meaning? The answer is a number, and it comes from the component’s content.

The program below computes this number for three components of the measurement station page.

// breakpoints.mjs — deriving breakpoints from content
// The page's horizontal padding is 24px on each side.
const PADDING = 24 * 2;

// 1) Text column: body text 18px, average character width 0.5em -> 9px/character
const CHAR_WIDTH = 9;
const MEASURE_MIN = 45;   // fewest readable characters
const MEASURE_MAX = 75;   // most readable characters
const textMin = MEASURE_MIN * CHAR_WIDTH;
const textMax = MEASURE_MAX * CHAR_WIDTH;

// 2) Measurement cards: the narrowest size a card stays meaningful at, and the gap
const CARD = 200;
const CARD_GAP = 16;
const cardThreshold = (n) => n * (CARD + CARD_GAP) - CARD_GAP;

// 3) Page layout: the narrowest sizes the main section and aside need to sit side by side
const MAIN_MIN = 480;
const ASIDE_MIN = 260;
const PAGE_GAP = 24;
const twoColumnThreshold = MAIN_MIN + ASIDE_MIN + PAGE_GAP;

console.log("--- 1) text column ---");
console.log(`body text 18px, 1 character ~ ${CHAR_WIDTH}px`);
console.log(`measure ${MEASURE_MIN}-${MEASURE_MAX} characters -> column ${textMin}-${textMax}px`);
console.log(`viewport counterpart: ${textMin + PADDING}-${textMax + PADDING}px`);
console.log(`line grows past upper bound: threshold ${textMax + PADDING}px`);

console.log("\n--- 2) measurement cards ---");
console.log(`card narrowest ${CARD}px, gap ${CARD_GAP}px`);
console.log("columns  content area needed  viewport threshold");
for (let n = 1; n <= 5; n++) {
  console.log(
    `${String(n).padStart(7)}  ${String(cardThreshold(n)).padStart(19)}  ${String(cardThreshold(n) + PADDING).padStart(18)}`,
  );
}

console.log("\n--- 3) page layout ---");
console.log(`main section narrowest ${MAIN_MIN}px + aside narrowest ${ASIDE_MIN}px + gap ${PAGE_GAP}px`);
console.log(`narrowest content area where two columns fit: ${twoColumnThreshold}px`);
console.log(`viewport threshold (lower bound): ${twoColumnThreshold + PADDING}px`);

console.log("\n--- every derived threshold ---");
const thresholds = [
  { name: "card 2 columns", px: cardThreshold(2) + PADDING, query: false },
  { name: "card 3 columns", px: cardThreshold(3) + PADDING, query: false },
  { name: "text upper bound", px: textMax + PADDING, query: false },
  { name: "page two columns*", px: twoColumnThreshold + PADDING, query: true },
  { name: "card 4 columns", px: cardThreshold(4) + PADDING, query: false },
  { name: "card 5 columns", px: cardThreshold(5) + PADDING, query: false },
];
thresholds.sort((a, b) => a.px - b.px);
console.log("threshold(px)  component            query needed");
for (const t of thresholds) {
  console.log(
    `${String(t.px).padStart(13)}  ${t.name.padEnd(20)} ${t.query ? "yes" : "no (intrinsic sizing is enough)"}`,
  );
}
const toWrite = thresholds.filter((t) => t.query);
console.log(`\ntotal thresholds: ${thresholds.length}, queries to write: ${toWrite.length} (${toWrite.map((t) => t.px + "px").join(", ")})`);
console.log("* lower bound from the sum of the narrowest sizes; rises with track definition");
--- 1) text column ---
body text 18px, 1 character ~ 9px
measure 45-75 characters -> column 405-675px
viewport counterpart: 453-723px
line grows past upper bound: threshold 723px

--- 2) measurement cards ---
card narrowest 200px, gap 16px
columns  content area needed  viewport threshold
      1                  200                 248
      2                  416                 464
      3                  632                 680
      4                  848                 896
      5                 1064                1112

--- 3) page layout ---
main section narrowest 480px + aside narrowest 260px + gap 24px
narrowest content area where two columns fit: 764px
viewport threshold (lower bound): 812px

--- every derived threshold ---
threshold(px)  component            query needed
          464  card 2 columns       no (intrinsic sizing is enough)
          680  card 3 columns       no (intrinsic sizing is enough)
          723  text upper bound     no (intrinsic sizing is enough)
          812  page two columns*    yes
          896  card 4 columns       no (intrinsic sizing is enough)
         1112  card 5 columns       no (intrinsic sizing is enough)

total thresholds: 6, queries to write: 1 (812px)
* lower bound from the sum of the narrowest sizes; rises with track definition

Three components, three separate sources of measurement.

The text column’s size comes from typography. Below 45 characters per line, the eye jumps lines too often; above 75, it becomes hard to find the start of the next line after a line break. In body text at an eighteen-unit base size, character width is roughly nine units; the two bounds come out to 405 and 675 units.

The cards’ size comes from the content itself: in a measurement card, the label, value, and unit must be readable side by side, and this breaks down below 200 units. How many columns fit follows from there.

The page layout’s size is the sum of the two sections’ sizes. The narrowest space they need to sit side by side is the sum of their two narrowest sizes and the gap between them.

The resulting list holds six thresholds, and none of them is named after a device. The numbers are odd: 464, 680, 723, 812. This oddness is not a flaw, it is the sign that the measure really does come from content.

Thresholds That Are Never Written

Of the six thresholds, only one requires conditional style. The remaining five are met automatically by the intrinsic sizing tools built in earlier lessons.

The card grid is defined with repeat(auto-fill, minmax(200px, 1fr)). Column count is computed from container width, and the 464, 680, 896, and 1112 thresholds are crossed as a natural result of that computation. Writing these thresholds with a query would mean doing the same computation by hand a second time.

The text column’s upper bound is given with max-inline-size; the measure never exceeds 675 units, and the remaining space goes to the margin. This too needs no condition.

What is left is the page layout itself. A grid-template-areas declaration is a structure, not a measure; moving from a two-column layout to a one-column one requires the declaration to be replaced by another declaration. Intrinsic sizing can compute a measure, it cannot choose a structure.

The criterion follows from this: if the measure changes, intrinsic sizing; if the structure changes, conditional style.

Track Definition Shifts the Threshold

The 812 figure above is the sum of the narrowest sizes, and it is a lower bound. The real threshold depends on how the tracks are defined.

// tracks.mjs — where the threshold comes from: deriving it from track definitions
const PADDING = 48;      // total horizontal padding on both sides of the page
const GAP = 24;           // gap between the two columns
const MAIN_MIN = 480;     // the narrowest size the main section stays meaningful at
const ASIDE_MIN = 260;    // the narrowest size the aside stays meaningful at

// Sizing minmax(min, Nfr) tracks: if a track's share falls below its minimum,
// the track is frozen at its minimum and the remaining space is redistributed.
function tracks(viewport, definition) {
  let remaining = viewport - PADDING - GAP;
  const result = definition.map((t) => ({ ...t, size: null }));
  let changed = true;
  while (changed) {
    changed = false;
    const open = result.filter((t) => t.size === null);
    const frTotal = open.reduce((s, t) => s + t.fr, 0);
    for (const t of open) {
      const share = (remaining * t.fr) / frTotal;
      if (share < t.min) {
        t.size = t.min;
        remaining -= t.min;
        changed = true;
        break;
      }
    }
    if (!changed) for (const t of open) t.size = (remaining * t.fr) / frTotal;
  }
  return result;
}

const definitions = [
  ["minmax(0, 2fr) minmax(0, 1fr)", [{ name: "main", fr: 2, min: 0 }, { name: "aside", fr: 1, min: 0 }]],
  ["minmax(480px, 2fr) minmax(260px, 1fr)",
    [{ name: "main", fr: 2, min: MAIN_MIN }, { name: "aside", fr: 1, min: ASIDE_MIN }]],
];

for (const [name, definition] of definitions) {
  console.log(`--- ${name} ---`);
  console.log("viewport     main col   aside col  main ok      aside ok     total overflow");
  for (const W of [768, 812, 852, 900, 1024]) {
    const s = tracks(W, definition);
    const main = s[0].size, aside = s[1].size;
    const overflow = main + aside + GAP - (W - PADDING);
    console.log(
      `${String(W).padStart(9)}  ${main.toFixed(1).padStart(9)}  ${aside.toFixed(1).padStart(9)}  ` +
        `${(main >= MAIN_MIN ? "yes" : "NO").padStart(11)}  ` +
        `${(aside >= ASIDE_MIN ? "yes" : "NO").padStart(11)}  ${overflow.toFixed(1).padStart(14)}`,
    );
  }
  // the first width at which both tracks meet their narrowest size without overflow
  let threshold = 300;
  while (threshold < 2000) {
    const s = tracks(threshold, definition);
    const overflow = s[0].size + s[1].size + GAP - (threshold - PADDING);
    if (s[0].size >= MAIN_MIN && s[1].size >= ASIDE_MIN && overflow <= 0.001) break;
    threshold += 1;
  }
  console.log(`derived breakpoint: ${threshold}px\n`);
}

console.log("--- comparison with a threshold taken from device names ---");
const PRESET = 768;
for (const [name, definition] of definitions) {
  const s = tracks(PRESET, definition);
  const overflow = s[0].size + s[1].size + GAP - (PRESET - PADDING);
  const narrow = s[0].size < MAIN_MIN || s[1].size < ASIDE_MIN;
  const diagnosis = narrow ? "tracks are below their narrowest size" : `tracks are adequate but ${overflow.toFixed(0)}px overflow`;
  console.log(`${PRESET}px, ${name}`);
  console.log(`  main ${s[0].size.toFixed(1)}  aside ${s[1].size.toFixed(1)}  -> ${diagnosis}`);
}
--- minmax(0, 2fr) minmax(0, 1fr) ---
viewport     main col   aside col  main ok      aside ok     total overflow
      768      464.0      232.0           NO           NO             0.0
      812      493.3      246.7          yes           NO             0.0
      852      520.0      260.0          yes          yes             0.0
      900      552.0      276.0          yes          yes             0.0
     1024      634.7      317.3          yes          yes             0.0
derived breakpoint: 852px

--- minmax(480px, 2fr) minmax(260px, 1fr) ---
viewport     main col   aside col  main ok      aside ok     total overflow
      768      480.0      260.0          yes          yes            44.0
      812      480.0      260.0          yes          yes             0.0
      852      520.0      260.0          yes          yes             0.0
      900      552.0      276.0          yes          yes             0.0
     1024      634.7      317.3          yes          yes             0.0
derived breakpoint: 812px

--- comparison with a threshold taken from device names ---
768px, minmax(0, 2fr) minmax(0, 1fr)
  main 464.0  aside 232.0  -> tracks are below their narrowest size
768px, minmax(480px, 2fr) minmax(260px, 1fr)
  main 480.0  aside 260.0  -> tracks are adequate but 44px overflow

Two definitions, two different thresholds.

In the first definition, the ratio is locked: the aside always takes a third of what remains, at every width. For the aside to reach 260 units, a third has to equal 260; the threshold rises to 852 units. The 812 that is the sum of the lower bounds is not enough here, because the main section is taking more than it needs.

In the second definition, the lower bounds are written into the tracks. The tracks settle into their narrowest sizes first, and the growing space is split by ratio afterward. The threshold comes down to the sum of the lower bounds, 812 units. This definition gains the page 40 units of width and puts the breakpoint at the same place as the computation’s first result.

The last block shows two separate ways an externally chosen threshold breaks. At the 768-unit threshold taken from device names, in the first definition the tracks stay below their narrowest size: the layout applies but the content gets squeezed. In the second definition the tracks keep their lower bounds, but this time their sum exceeds the container by 44 units: horizontal overflow results.

Both results come from the same cause. When a threshold is not derived from content, the bond between the measures and the threshold breaks, and the fault shows up either as squeezing or as overflow.

Starting From the Narrow Side

Once the breakpoint is found, one writing decision remains: which layout should the base style describe?

If the base style describes the narrow layout, conditions are added as width grows. The single-column arrangement is already written; the condition only builds the second column.

If the base style describes the wide layout, conditions are added as width shrinks, and each condition carries declarations that undo what the base built: zeroing area names, bringing the track count down to one, shrinking margins.

Starting from the narrow side eliminates the undoing declarations. There is a second reason too: content has to be prioritized in a narrow space. Which section comes first, which information can be carried at all, is decided at the narrow layout; the wide layout is built on top of these decisions. Working in the reverse direction leaves the narrow layout as whatever is left over from the wide one.

/* station.css — step 7: narrow base, one threshold */
.station-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas:
    "masthead"
    "measurements"
    "aside"
    "location";
  gap: var(--spacing-1);
}

.measurement-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--spacing-0);
}

.station-notes { max-inline-size: 675px; }

There is not a single query in the base style. The card grid gains columns as width grows, the text column stops at its upper bound, the page is single-column. Only at the 812-unit threshold does the structure change; that declaration will be written in the next lesson.

The Viewport Declaration

One precondition is required for all this computation to hold. On small-screen devices, the browser can assume a wide virtual viewport, to make pages not written for a narrow screen readable, and shrink the result to fit. In this case, the width CSS sees is not the screen’s real width, and the derived thresholds never trigger at all.

The viewport meta tag introduced in the Web Fundamentals and HTML course turns this assumption off:

<meta name="viewport" content="width=device-width, initial-scale=1">

The declaration sets the width CSS sees equal to the device’s own width. Responsive style’s first line is not in the stylesheet, it is in the document’s head.

Summary

  • Responsiveness is three components present together: fluid measures, media that never exceeds its container, and conditional style where needed.
  • A breakpoint is not a device width; it is a number derived from the narrowest size a component can shrink to before losing its meaning. Derived numbers are not round.
  • If the measure changes, intrinsic sizing is enough; if the structure changes, conditional style is needed. On the station page, only one of six thresholds turns into a query.
  • Track definition shifts the threshold: 852 units with a locked-ratio definition, 812 units with a definition whose lower bounds are written in.
  • When the threshold is chosen externally, the fault appears in two forms — tracks fall below their narrowest size, or their sum exceeds the container and produces overflow.
  • When the base style describes the narrow layout, conditions add; starting from the wide layout means every condition carries undoing declarations.

Next Step

The derived threshold is in hand: 812 units. What remains is how this number gets written in the stylesheet. A condition is not built with width alone; style can also branch on height, the screen’s orientation, or whether the output is going to paper or to a screen. The next lesson takes up the writing of conditional style: which features a condition can look at, how two conditions are combined, and which rule wins at the width sitting at the edge of two ranges.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close