---
title: 'The Principle of Responsive Design'
source: 'https://academia.sh/en/courses/layout-and-responsive-design/responsive-design-principle'
course: 'Layout Systems and Responsive Design'
language: en
updated: '2026-08-17T18:09:28+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```js
// 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.

```js
// 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.

```css
/* 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:

```html
<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.
