Red Blob Games Responsive Design
Model layout as a function from browser width to content width, and demand it be monotonic (wider browser never narrows content) and continuous (no jumps) — then implement it as a piecewise linear function in CSS (calc() with a vw slope term, breakpoints only where segments meet).
TLDR
- Model layout as a function from browser width to content width, and demand it be monotonic (wider browser never narrows content) and continuous (no jumps) — then implement it as a piecewise linear function in CSS (
calc()with avwslope term, breakpoints only where segments meet). - Scale font size with the same linear-interpolation trick so line length stays ~66–75 characters as paragraphs widen; but keep px font sizes inside scaled diagrams or scaling applies twice.
- Each embedded element type needs its own responsive recipe: svg via
viewBox, img viamax-width:100%, canvas viawidth:100%+ devicePixelRatio rescale, flexbox for side-by-side; iframes and absolute positioning largely resist conversion.
Caveman
Content width is a function of browser width. Draw it.
ELI5
Instead of asking “what should my page look like on a phone vs. a tablet vs. a laptop?”, ask “as the window gets wider, how should the text column grow?” Draw that as a simple graph: at small sizes the text takes almost everything, in the middle it shares new space with the margins, and past a cap the margins absorb all growth. Then translate each straight segment of that graph directly into a CSS calc() rule.
Trunk → Branches
Trunk: Design responsive layout as a monotonic, continuous function from browser width to content width — device-independent math, not device-specific breakpoints. Branches:
- Fixed widths fail across device classes: one 600px-wide “scale to fit” layout made text too small on phones, too big on tablets, and paragraphs too narrow on desktops (§ intro).
- Visualize the function first: plotting browser width → layout exposed that even a beautiful, admired site was responsive but not monotonic — paragraphs got narrower going from 767→768px (§2).
- Piecewise linear content width: three segments —
calc(100vw - 36px)(slope 1, phones),calc(330px + 33vw)(slope ⅓, mid), constant660pxpast 1000px (slope 0) — where thevwcoefficient is the slope (§3). - Font size interpolates too:
calc(10px + 1vw)capped at 20px keeps the paragraph-width÷font-size ratio ≈33 (~66 chars/line), derived by picking two good endpoints and lerping between them (§4). - Two-column layouts are the same math with more segments: either grow both columns equally, or fill the text column first then grow the diagram column — each strategy is just two more breakpoints computed from the column-width sums (§6).
- Per-element conversion is manual: old content doesn’t declare whether an element should shrink-to-fit or match paragraph width, so pages must be converted one at a time; interactive elements additionally need mouse-coordinate rescaling (§7).
- Verify with screenshot diffing: headless Firefox/Chrome screenshots at several widths, before vs. after, on a quick self-built comparison page (§8).
Key Patterns
- Pattern: Expressing a design constraint as a function with required properties (monotonic, continuous) works because it turns aesthetic judgment into checkable math — transfers to any parametric design problem (spacing scales, type ramps, chart sizing).
- Pattern: Linear interpolation between two hand-picked good endpoints (16px@600w, 20px@1000w →
10px + 1vw) works because you only need taste at the extremes and math fills the middle — transfers to fluid typography (aka “CSS locks”) and animation easing anchors. - Pattern: Breakpoints derived from content arithmetic (margin + column + gutter sums like 1110px, 1374px) work because layout switches exactly when content genuinely fits — transfers anywhere over device-specific breakpoints (768px iPad-ism).
Stress Test
- Hidden assumption: the page is a single centered content column of flowing text + figures. The whole function-of-width model presumes one primary axis to allocate.
- Best counterargument: almost nobody resizes their browser continuously — the author admits users won’t notice non-monotonic jumps. Standard device-bucket breakpoints (the approach the admired site used) are simpler to reason about, better supported by frameworks, and produce indistinguishable results for real visitors.
- Fails when: layouts use iframes (CSS won’t preserve aspect ratio or resize contents) or complex absolute positioning — the author gave up and left the visibility page at fixed 600px. Also strains app-like UIs (dashboards, multi-pane tools) with no single content column.
- Fact vs opinion: “readable paragraphs are ≤75 characters” is a typography guideline, not established fact. Preferring monotonic/continuous over device breakpoints is the author’s stated design taste — he concedes most users would never notice the difference; likewise “not convinced” the in-CSS calculator beats precomputed values (§5).
In Practice
- Set content width as a piecewise linear function:
--body-width: calc(100vw - 36px), thencalc(330px + 33vw)past a 550px breakpoint, then constant660pxpast 1000px — thevwcoefficient is the slope of each segment — [§3] - Scale fonts the same way:
--font-size: calc(10px + 1vw)capped at 20px; derive constants by choosing target sizes at two widths and applying the lerp formula; keep fixed px fonts inside scaling diagrams to avoid double-scaling — [§4] - Modern simplification (as of ~2020+): replace breakpoint pairs with
min()/max()/clamp(); the author’s 2026 revision computes the whole layout in CSS custom properties withmin()andround(down, …, 1px). Units:remhonors the user’s global default-font-size, px forces per-site zoom — he later moved to rem for accessibility, and regrets non-round maxes (41.25rem; 45/50rem would give 720/800px images) — [§5, §9] - Per-element responsive recipes: svg →
viewBoxinstead of width/height (+max-widthto shrink-but-not-grow, wrapped in<figure>formargin:autocentering); img →max-width:100%; height:auto; canvas →width:100%in CSS while multiplying mouse coords bycanvas.width / canvas.offsetWidthand sizing the backing store bydevicePixelRatio; side-by-side → flexbox withflex-wrap+flex-basis: calc(var(--body-width)/2)— [§7] - Two-column breakpoints come from content sums: e.g. margins + 2×528px columns + gutters = 1110px to start two columns; = 1374px when both hit 660px max (equal-growth strategy), or start the diagram column at 400px only after the text column is full width (text-priority strategy) — [§6]
- Regression-test visually: headless
firefox -screenshot --window-size=$width,4000 "$url"/chrome --headless --screenshotat several widths, diff before/after on a comparison page — [§8]
Connections
- ASCII Text Images in p5.js — a browser figure with fixed-px inner type — that note renders ASCII as monospace DOM text tuned by
line-height; this note’s rule to keep px fonts inside scaled diagrams (scale the container, not the glyphs) is what keeps such a figure aligned on a fluid page.