Responsive Typography
Fluid type with clamp() that still honours the user's font size, a measure defined in characters rather than pixels, and layouts that survive a translation two-thirds longer than the English it was designed around.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
How do I make text scale with the space available without breaking zoom, readability, or the German build?
A person wants to read comfortably: text large enough to see, lines short enough to track, and labels they can actually read in their own language.
Set a base font size in pixels, add a couple of breakpoints where headings get bigger, and size buttons and nav items to fit their labels exactly as the design specifies.
A font size in px ignores the user's browser font-size setting entirely. Someone who raised their default because they cannot read the default gets no change at all, and the page looks perfect to everyone who never changed it.
- A font size in
pxignores the user's browser font-size setting entirely. Someone who raised their default because they cannot read the default gets no change at all, and the page looks perfect to everyone who never changed it. - A pure viewport-unit font size —
font-size: 4vw— fails in a subtler way: it responds to the viewport but not to the user's text preference, so text-only zoom does nothing and the resize-text requirement is unmet. - A measure fixed in pixels only produces the intended line length at one font size. Raise the font and the same box holds five words per line; a wall of four-word lines is measurably harder to read than a wall of prose.
- A button sized to fit "Save" clips "Speichern", and a nav bar tuned to fit six English labels overflows with six German ones. The layout encoded the length of one language's strings (Internationalization).
- A sentence assembled by concatenation —
'Delete ' + n + ' items'— bakes English word order and English plural rules into code, and produces text that is wrong rather than merely long in many languages.
What is actually happening
In the browser, not in the framework.
remresolves against the root font size, which is the user's browser setting unless you overrode it. That is the mechanism by which a user's preference reaches your text, and settinghtml { font-size: 16px }is how it gets silently disconnected.clamp(min, preferred, max)gives a floor, a scaling expression and a ceiling. The idiom for fluid type isclamp(1rem, 0.95rem + 0.25vw, 1.125rem): theremterm keeps the user's setting in play and thevwterm adds the fluid response on top.- The
chunit is the advance width of the "0" glyph in the element's own font, so a measure inchscales with the text.max-inline-size: 68chholds a roughly constant number of characters per line as the font size changes (Fluid Layout First). - Line height as a unitless number multiplies the element's own font size and inherits as a ratio rather than a computed length, which is why a unitless
line-heightis correct onbodyand apxone is a trap (Inheritance and Computed Style). text-wrap: balancedistributes a short block's lines evenly, andtext-wrap: prettyavoids orphans in longer text. Both are typesetting decisions the browser makes at layout time rather than things you position by hand.- Text expansion is a property of translation, not of the layout: the same message in another language routinely occupies substantially more space, and short strings expand proportionally the most. Sizing a box to its English content encodes an assumption about the whole product's language set (Timezones and Locale Formatting).
What this makes the browser do
And which of it is avoidable.
- Text layout is line breaking, shaping and measurement per line, and it is redone whenever the available inline size or the font changes. A fluid measure means this happens on every resize step rather than at four breakpoints.
- A web font arriving after first paint changes every glyph advance, so every line box is re-measured and re-broken. That is the reflow behind the font-swap shift (Images and Fonts).
ch-based sizing requires font metrics, so achmeasure computed against the fallback font changes when the web font loads. Matching fallback metrics is what keeps the change small (Visual Stability).text-wrap: balancecosts more than normal wrapping because the browser tries several break arrangements; implementations bound it to short blocks for exactly that reason (The Cost of a Change).- Loading a translation bundle at runtime replaces text after layout, so every string change re-measures the boxes containing it (Layout Thrashing).
Fluid type that still belongs to the user
There are two inputs to how large text should be, and a common mistake is honouring only one. The available space is one input; the size the user asked their browser for is the other. A pixel size ignores the second. A pure viewport-unit size also ignores the second, while feeling modern enough that nobody checks.
The rem + vw preferred term inside clamp() is the idiom that keeps both. The rem half moves with the user's setting, the vw half adds the fluid response, and the bounds stop either from running away.
1/* Do NOT set html { font-size: 16px } or 62.5% — the root belongs to the user. */2 3:root {4 /* Each step: a floor, a (rem + vw) preferred term, a ceiling. */5 --step-0: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);6 --step-1: clamp(1.25rem, 1.15rem + 0.6vw, 1.5rem);7 --step-2: clamp(1.5rem, 1.2rem + 1.6vw, 2.5rem);8 9 --measure: 68ch; /* characters, so it scales with the font */10}11 12body { font-size: var(--step-0); line-height: 1.6; }13 14h2 {15 font-size: var(--step-2);16 line-height: 1.15;17 text-wrap: balance; /* progressive enhancement over normal wrapping */18}19 20.prose > p {21 max-inline-size: var(--measure); /* logical, so RTL and vertical work too */22 text-wrap: pretty;23}24 25/* Labels: sized by content, floored for touch, never fixed. */26.button {27 min-inline-size: 2.75rem;28 min-block-size: 2.75rem;29 padding-inline: 1rem;30 white-space: normal; /* let a long translation wrap rather than clip */31}Two lines carry most of the weight. The rem term inside each clamp() is what keeps the user's font-size setting connected — replace it with pure vw and text-only zoom stops working while the page still looks fluid. And white-space: normal on the button is what turns a German label from a clipped one into a two-line one.
Measure belongs to the font, not to the box
Line length is one of the few typographic variables with real evidence behind it: too long and the eye loses its place returning to the next line, too short and the reading rhythm breaks every few words. What matters is characters per line, which is a property of the text, not of the container.
Expressing the measure in pixels produces the intended line length at exactly one font size — the one the developer had. Every other user gets a different, unconsidered number of characters per line, and the users most affected are the ones who raised their font size because reading is already hard.
- Unitless
line-heightinherits as a ratio and is recomputed against each element's own font size; apxline-height inherits as a fixed length and clips larger text (Inheritance and Computed Style). text-wrap: balancefor headings and short blocks,text-wrap: prettyfor body copy — both are enhancements over a layout that must already be acceptable without them.overflow-wrap: anywhereon user-generated text stops a pasted URL from setting the width of its container (Fluid Layout First).- Set
langcorrectly on the document and on any element in a different language: hyphenation, quotation marks, font fallback and screen-reader pronunciation all depend on it (Document Structure and Reading Order).
.prose {
max-width: 640px;
font-size: 20px;
}
/* correct at exactly one font size, in exactly one writing mode */.prose {
max-inline-size: 68ch;
font-size: var(--step-0);
}
/* holds ~68 characters per line at any font size, in any writing mode */ch is defined against the element's own font, so when the user raises their default size the box grows with the text and the characters-per-line stays roughly constant — which is the thing readability actually depends on. The pixel version keeps the box and shrinks the line to four or five words. max-inline-size rather than max-width also means the constraint follows the writing mode, so a right-to-left or vertical translation is constrained along the axis the text actually flows in (Internationalization).
The German build
Every fixed-width label in an interface is an unstated claim that no translation of its text is longer than the English. That claim is false in a way that is well documented and easy to plan for, and it fails worst on exactly the strings a designer is most likely to size precisely: single-word buttons and navigation items.
The remedy is not to make everything wider. It is to stop sizing by content length at all — floors and padding instead of fixed widths, wrapping instead of nowrap, and pseudo-localisation in CI so the failure is found by a build rather than by a translator filing a ticket.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Button width fixed to fit the English label | The German label is clipped or ellipsised and the user cannot tell which button deletes | The width was derived from one string in one language | Size by content with a min-inline-size floor for the touch target, and let the row wrap (Fluid Layout First). |
Navigation with white-space: nowrap on one line | Items overflow and the last one becomes unreachable | The layout assumed a total label length | Allow wrapping, or move overflow into a menu driven by the container's own width (Container Queries). |
| Sentence assembled by concatenation | Grammatically wrong in languages with cases or more than two plural forms; sometimes the word order is reversed | Concatenation encodes English syntax in code | Full parameterised messages with plural rules, formatted through Intl (Internationalization). |
| Icon-only controls "because labels do not translate" | Nobody is sure what the icon means, and the control may announce nothing useful | The width problem was solved by deleting the information | Keep the visible label and give it room. Where space genuinely forbids it, the accessible name is mandatory (Semantics Before ARIA). |
| Text baked into an image for typographic control | It cannot be translated, selected, searched, zoomed or read aloud, and it blurs on dense screens | Layout control bought with content | Real text over the image; the image carries the picture only (Responsive Images). |
| A table with fixed column widths | Headers wrap to four lines in one language and leave half the row empty in another | Column widths tuned against one set of strings | Content-based track sizing with sensible minima, and a horizontal scroll container as the honest fallback (Grid: Two Dimensions at Once). |
source (en) de fr ja
---------------------------------------------------------------------------
Save Speichern Enregistrer 保存
Settings Einstellungen Paramètres 設定
Undo Rückgängig machen Annuler 元に戻す
Delete account Benutzerkonto löschen Supprimer le compte アカウントを削除
Two patterns to take from this:
1. Short strings expand proportionally the most. Localisation guidance
puts growth for a very short source string well above 100%, tapering
as strings get longer. A button label is therefore the WORST place
for a fixed width and a paragraph is the safest.
2. Expansion is not the only axis. CJK text is often shorter but taller
per glyph and has different break rules; some scripts need more line
height; some are read right to left. "Make the box wider" is only
the fix for one of these.How to build it
Most important first.
- Never set a pixel font size on
html. Let the root be the user's setting and express your scale inremfrom there — this single rule is most of typographic accessibility. - Build a small type scale as custom properties, each a
clamp()whose preferred term isrem + vw, nevervwalone. Two or three steps cover most interfaces (Design Tokens). - Define the measure in
chon the text element withmax-inline-size, not inpxon a wrapper. It then stays correct at every font size and in every writing mode (Internationalization). - Size controls by their content with a minimum, never by a fixed width.
min-inline-sizefor the touch target,padding-inlinefor the breathing room, and let the label decide the rest (Media Queries Beyond Width). - Design labels with room to grow, and pseudo-localise early: run the interface with artificially lengthened strings before any translation exists, so the breakage is found by you rather than by a translator.
- Use full parameterised messages with plural and gender rules through the platform's
Intlformatting rather than concatenating fragments, so translators receive a sentence rather than a jigsaw (Internationalization).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Text must be resizable substantially without loss of content or function. Every size in
remwith achmeasure satisfies this almost by construction; every size inpxfails it silently. - Line length is an accessibility requirement, not a taste. WCAG's visual-presentation guidance names a maximum characters-per-line for readable blocks, and users with dyslexia and low vision are the ones it protects — which is exactly why the measure belongs in
ch. - Line spacing and paragraph spacing must survive user overrides. A user stylesheet or extension that increases spacing must not cause clipping, which means containers need to grow with their content rather than being fixed in height.
- Truncation removes information.
text-overflow: ellipsishides text visually while leaving it in the accessibility tree, so a sighted user and a screen-reader user get different content from the same element. Decide which is correct and make them match (Semantics Before ARIA). - Text in images cannot be zoomed, translated, selected or read aloud, and it renders soft on dense displays. Real text over a picture is the accessible and the responsive answer at the same time (Responsive Images).
What can go wrong
- A
clamp()whose preferred term is purevw. It looks fluid, it is fluid, and it completely ignores the user's font-size setting — a resize-text failure that is invisible on every machine where that setting was never changed. html { font-size: 62.5% }to makeremarithmetic convenient. It scales the user's chosen size by a factor they did not ask for, so a user who set a large default gets a smaller page than they configured.- A measure in
chon a container that also sets a different font. Thechis resolved against the container's font, not the text's, so the measure is wrong by whatever the two fonts differ by. white-space: nowrapon navigation to keep it on one line, which converts a translated overflow into an unreachable menu item rather than a wrapped one.- Fixing expansion by shrinking the font in long-string languages, which makes exactly the users with the longest words read the smallest text.
- The mitigation failing: labels given room to grow, but a
text-overflow: ellipsisadded as a safety net, so the string is now truncated silently and no test notices because the DOM still contains the full text.
- A web font arriving after first paint changes glyph metrics, so text the user has already begun reading re-wraps and moves. Matched fallback metrics reduce the size of the jump but cannot remove it (Images and Fonts).
- A translation bundle loaded after render replaces every string, re-measuring every box — a shift that lands after the page appeared finished (Visual Stability).
- A
ch-based measure computed against the fallback font is recomputed when the real font loads, so the line length the user first sees is not the one they end up with.
- Translated strings are content from a pipeline, and a translation management system is a place where markup can be introduced. Interpolating a translated string as HTML is an injection path with an unusually long supply chain (Cross-Site Scripting).
- User-supplied text is a layout input. A display name of 300 characters with no break opportunities can overflow, obscure controls, or push an action out of the viewport — constrain length at the boundary, not only in CSS (Sanitization and Trusted HTML).
- Homoglyph and bidirectional-control characters in user text can make a rendered string read differently from what it contains. This is a display-layer problem with a security consequence, and stripping bidi controls from user-generated names is a standard mitigation (Parse, Validate, Authorize, Process).
- "
remis justpxdivided by 16." It is a multiple of the *user's* root size, which is 16 only by default. That difference is the entire accessibility point. - "Fluid type means viewport units." Viewport units alone break the user's font-size setting. The
rem + vwpreferred term is the idiom precisely because it keeps both inputs. - "Text expansion is only a problem for German." It affects every target language differently and it is worst for short strings in general; treating one language as the special case just means you find the bugs one language at a time.
- "Icons avoid the translation problem." They avoid the width problem by removing the information, and an icon-only control still needs an accessible name — which is a string, which is translated (Semantics Before ARIA).
- "We can fix expansion in CSS with
text-overflow." Truncating a label makes it unreadable rather than merely tight, and it hides the bug from every test that reads the DOM.
Measuring it, and what changes in the field
- Set your browser's default font size to a large value and use the product. Nothing else finds pixel-sized boxes as fast (A Method for Frontend Bugs).
- Run the interface pseudo-localised — every string mechanically lengthened and accented — in CI, and screenshot the results. Expansion bugs are visual and this is the only cheap way to see them (Visual Regression Testing).
- The elements panel's computed values show what a
clamp()resolved to at the current width, which is the quickest way to find a preferred term that never wins. - Font-swap layout shift shows up in field layout-shift attribution as text-containing elements moving shortly after load (Visual Stability).
- Right-to-left is a rendering mode you can switch on with one attribute; doing so immediately shows every place a physical property was used where a logical one was needed (Internationalization).
- On a slow network, the translation bundle and the web font both arrive after first paint, so the text the user first sees may be in the wrong language *and* the wrong metrics (Loading: Why Content Arrives Late).
- On a narrow viewport, expansion and measure collide: the language with the longest words is also the one with the fewest break opportunities per line, so justification and hyphenation matter more.
- With a large user font size, a layout can effectively be at a phone measure on a desktop screen. That combination is real and is almost never tested.
- In a vertical writing mode, physical properties are simply wrong —
max-widthconstrains the wrong axis, and onlymax-inline-sizebehaves (Internationalization). - In a long-lived tab, switching language at runtime replaces every string and re-measures every box, which is a full relayout rather than a repaint (Long-Lived Clients and Version Skew).
- Fluid type means the heading is never exactly the size in the design file at any width, which is a real cost when a brand has a typographic specification someone signed off (Design Systems).
- A
clamp()type scale is harder to read than a list of sizes. Debugging "why is this 22px" through three custom properties and a viewport term is genuinely worse than reading a media query. - Designing for expansion means the English build has visible slack — buttons wider than their labels need, nav bars with room to spare — and someone will ask to tighten it.
- Parameterised messages with plural rules are more machinery than string concatenation, and they require translators, tooling and a review process. They are also the only thing that produces correct sentences in languages with more than two plural forms.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERAL
rem,ch,clamp()and unitlessline-heightbehave identically across engines; the interoperable disagreements in typography are about font fallback metrics and hyphenation dictionaries, not about the units. - SPEC-EVOLVING
text-wrap: balanceandtext-wrap: prettyare recent additions whose implementations differ in which cases they apply to and how many lines they will consider, so treat them as progressive enhancement over a layout that is already acceptable with ordinary wrapping. - SIMPLIFIEDThe expansion figures quoted in this lesson are the rules of thumb published in localisation guidance, not measurements of your strings; actual growth depends on the source string, the target language and the domain vocabulary, and the only reliable number comes from pseudo-localising your own product.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Software Design — a message is an interface with parameters, and concatenating fragments is the string equivalent of leaking an implementation detail across a boundary that translators have to work through.
- — Testing & Reliability Engineering — pseudo-localisation is a fault injection technique: deliberately deform an input to expose assumptions the happy path never exercises.