UX-strategy
How to Build a Type Scale: Every Size, Every Element
If your H1 is 48px, what should everything else be? A complete method for deriving a type scale from one size you have already decided, covering line height, tracking, weight, and every UI element from buttons to table cells.
Most typography advice stops at the part you already understand. Pick a nice font, keep your line length comfortable, do not use twelve weights. Fine. The question that actually blocks you is narrower and more annoying: you have settled on 48px for your page heading, and now you need to know what every other piece of text in the product should be.
Not just H2 and H3. The button label. The input field. The helper text under the input. The table header versus the table cell. The little count badge. The tooltip. Guessing each one is how you end up with eleven font sizes that are all slightly different for no reason.
Here is a method that answers all of it from one decision you have already made.
Start from the size you already know
Almost every type scale tutorial asks you to set a base body size and build upward. That is backwards from how the decision actually happens. You rarely start with body copy. You start with a hero, or a heading someone has already signed off, and you work outward from there.
So pin whichever size you have decided, and derive the rest in both directions. Every step up multiplies by your ratio, every step down divides by it.
| Role | Preview | px | rem | Line height | Tracking | Weight |
|---|
On iOS and Android
Your scale against what each platform ships by default. The gap is the
useful part: near zero means you can use the platform's own style
untouched, a large number means you are defining a custom one. Points, dp
and CSS pixels are all density-independent, so the numbers compare
directly. Android sp also scales with the reader's font-size
preference, so treat that column as a starting point rather than a fixed size.
| Role | Yours | iOS Dynamic Type | Material 3 |
|---|
Pin Body at 16 and you get 41.9 for H1. Pin H1 at 48 and you get 18.3 for body. Same scale, different anchor. That is the whole trick, and it is the reason this is worth automating rather than doing in your head.
The same generator, along with matching tools for spacing, colour contrast, radius, elevation and fluid sizing, lives on the design system toolkit page if you want the rest of the system rather than just the type.
Why the golden ratio needs a square root
The golden ratio, 1.618, is the one everybody reaches for. It is also, applied naively, a bad choice for interface work, and it is worth understanding exactly why before you either commit to it or abandon it.
Run it from a 16px base and you get this: 16, then 25.9, then 41.9, then 67.8, then 109.7. Downward it gives you 9.9 and then 6.1.
Two problems. The bottom of the scale is unusable, since nothing below about 11px belongs in a shipping interface. And the gap between 16 and 25.9 is a dead zone with nothing in it. You have no 18px lead paragraph, no 20px card title, no 13px caption. Those are not exotic requirements: they are most of a product.
This is not a matter of taste. Look at what shipped design systems actually do and none of them uses a single pure ratio from top to bottom. IBM Carbon's scale accelerates, adding 2px more to each increment every four steps. Material 3, Apple, Polaris and Atlassian all compress the small end and stretch the large end. The effective ratio in the 12px to 16px range is around 1.1, and around 1.3 at display sizes.
The fix that keeps the golden ratio honest is to step by its square root instead:
| rem | px | Role | Note |
|---|---|---|---|
| 0.618 | 9.9 | Micro | This is 1 over phi |
| 0.786 | 12.6 | Caption, helper, error | |
| 1.000 | 16.0 | Body, inputs | The anchor |
| 1.272 | 20.4 | Lead paragraph | |
| 1.618 | 25.9 | H3, card title | A full golden step from body |
| 2.058 | 32.9 | H2 | |
| 2.618 | 41.9 | H1 | Phi squared |
| 3.330 | 53.3 | Display | |
| 4.236 | 67.8 | Hero | Phi cubed |
Because the square root of phi multiplied by itself is phi, every second step is a true golden interval. Body to H3 is golden. H3 to H1 is golden. H1 to Hero is golden. The steps in between are the geometric mean of the two golden values that flank them, which is the most defensible way to split the difference.
The same constant turns up in colour, and it is worth knowing about. Rotate a hue by 137.5 degrees and you have the golden angle: 360 divided by phi squared, or 360 times one minus one over phi. It is the angle plants use to space leaves around a stem so that no leaf sits directly above another, and it is why sunflower seeds pack the way they do. Two steps of it give you three hues that never revisit each other. The design system toolkit offers it alongside the classic wheel intervals, though it scores it against the others rather than assuming the maths makes it better: whether it suits your particular hue is a separate question from whether the number is elegant.
There is a useful sanity check here. Round that scale and it reads 16, 20, 26, 33, 42, 53, 68. IBM Carbon's published scale runs 12, 14, 16, 18, 20, 24, 28, 32, 36, 42, 48, 54, 60, 68, 76. Four of those land exactly: 16, 20, 42 and 68. A ratio derived from a 2,000-year-old proportion and a scale hand-tuned by a corporate design team for dense enterprise software converge on the same numbers. That is a reasonable sign you are not fooling yourself.
Line height moves the opposite way to size
Leading exists to help your eye find the beginning of the next line. Large text already gives the eye plenty to track, so a generous ratio just leaves lines drifting apart. Small text needs proportionally more room.
So the ratio falls as size climbs, even though the absolute leading still grows. Carbon's expressive set shows this more clearly than any explanation: as size goes 20, 24, 28, 32, 36, 42, 48, 60, 76, the line-height ratio goes 1.4, 1.334, 1.286, 1.25, 1.22, 1.19, 1.17, 1.13.
Practical targets:
| Size | Line height | Result |
|---|---|---|
| 11 to 12px | 1.33 | 16px |
| 14px | 1.43 for prose, 1.29 in a table | 20px or 18px |
| 16px | 1.5 for prose, 1.375 when compact | 24px or 22px |
| 20px | 1.4 | 28px |
| 32px | 1.25 | 40px |
| 48px | 1.12 | 54px |
| 68px and up | 1.05 | 72px |
Two things in that table are worth pulling out.
The same size gets two different leadings depending on context. Carbon ships 14px at 20px leading for paragraphs and 14px at 18px leading for table rows and other dense UI. This is the single most useful idea in their system and almost nobody copies it. A table row is not prose, and giving it prose leading makes your data view sprawl.
Keep every line height divisible by four. Check the ramps: Material 3, Atlassian, Polaris and Tailwind all do this without exception. It is what lets type sit inside a 4pt spacing grid without fighting it. Carbon is the deliberate holdout, using unitless ratios that produce fractional line boxes.
That last rule collides with the golden ratio, and it is worth being honest about it. A golden line height on 16px body is 25.9px, which is off grid. Snap it to 24px and you get a 1.5 ratio, which happens to match Material 3's Body Large, Tailwind's text-base and Carbon's body-02 exactly. You can have a golden scale or golden leading. Take the scale, snap the leading.
Tracking: the rule everyone overstates
The principle is real. Letterforms are drawn with sidebearings tuned for a particular size, so blow them up and the gaps blow up too, and words start to disintegrate into separate letters. Shrink them and the letters crowd. So you tighten large text and loosen small text.
The numbers people quote are not real. You will constantly read that display type wants negative tracking of 0.02em to 0.04em. That figure exceeds every major published design system:
| System | Largest negative tracking |
|---|---|
| Shopify Polaris | 0.015em, and only at 20px and above |
| Material 3 | 0.0044em, on Display Large alone |
| IBM Carbon | None. Zero everywhere, positive on small text |
| Atlassian, GitHub Primer | No letter-spacing tokens at all |
| US Web Design System | 0.03em, its outermost token |
So 0.04em is not conservative practice dressed up as a rule. It is contemporary marketing-site styling, and if you apply it to a product UI it will read as mannered. Cap yourself at 0.03em negative and you are still at the outer edge of anything published.
Where tracking genuinely earns its place is the other direction: small uppercase labels. Butterick's guidance is 5 to 12 percent extra on caps, so 0.05em to 0.12em. An 11px uppercase eyebrow at 0.1em tracking is the single biggest typographic improvement available for the least effort. Lowercase body text at normal sizes needs nothing.
Weight: three is usually enough
Count what shipped systems actually ship. Carbon: three weights. Primer: four, and no bold at all. Material 3: three in practice. Polaris and Atlassian: four each. Apple recommends four and explicitly tells you to avoid Ultralight, Thin and Light because they fall apart at small sizes.
So: one regular for body, one emphasis weight at 500 or 600 for UI and small headings, one display weight at 700. A fifth weight is nearly always redundant, and every extra weight is another font file or another variable instance to check.
Two things worth stealing:
The same weight looks heavier as it gets larger. Stems scale with the glyph, so a 600 at 32px reads bolder than a 600 at 14px. Carbon compensates by dropping weight as size climbs, going 600 at 14px to 400 at 20px to 300 at 42px. Whether you follow them that far is a brand decision, but the effect is real.
Weight can carry hierarchy on its own. Apple's Headline and Body styles are both 17pt with 22pt leading. The only difference between them is Semibold against Regular. That works precisely because the two sit next to each other, and it saves you a size.
Measure, and the space around headings
Bringhurst's number is the one to know: 45 to 75 characters per line, with 66 as the ideal for a single column. Since a character averages roughly half an em, max-width: 66ch or about 33em gets you there. Butterick allows up to 90. Treat the range as a comfort finding rather than settled science, because reading speed often peaks at longer measures than preference does.
For the space around headings there is no authoritative published number, so here is the part that is defensible: the space above a heading must be larger than the space below it. A heading belongs to the content that follows, so the gap separating it from the previous block has to read as bigger. A symmetric margin: 1em 0 leaves headings floating ambiguously between two blocks, and it is the most common rhythm mistake there is.
A 2:1 ratio works. Set it in em and it scales with each heading automatically rather than needing a value per level.
One CSS trap: margin-top on a heading collapses with the previous element's margin-bottom in normal flow, but margins do not collapse inside flex or grid containers. The same tokens produce different spacing depending on the layout mode, which is why newer systems drop element margins and use gap on a flow container instead.
Every element, not just headings
This is the part the tutorials skip. Values below are the modal choice across Material 3, Apple, Carbon, Polaris and Atlassian.
| Element | Size | Line height | Weight | Tracking |
|---|---|---|---|---|
| Button | 14px | 1.0 to 1.25 | 500 to 600 | +0.007em, or +0.05em if uppercase |
| Input field | 16px on mobile, 14px desktop only | 1.4 | 400 | 0 |
| Field label | 12 to 14px | 1.33 | 400 to 500 | +0.02em |
| Placeholder | Same as the input value | 1.4 | 400 | 0 |
| Helper and error text | 12px | 1.33 | 400 | +0.027em |
| Nav item | 14px | 1.43 | 400, going to 600 when active | 0 |
| Table header | 14px | 1.29 | 600 | +0.011em |
| Table cell | 14px | 1.29 | 400 | +0.011em |
| Badge or chip | 11 to 12px | 1.0 to 1.33 | 500 | +0.04em |
| Tooltip | 12 to 14px | 1.33 | 400 | 0 |
| Breadcrumb | 12 to 14px | 1.33 | 400, current page 600 | 0 |
| Big number or KPI | 28px | 1.15 | 600 to 700 | 0 |
| Inline code | 0.875em | 1 | 400 | 0 |
Several of those carry a rule that is not obvious from the number alone.
Buttons and badges should use min-height and padding, never a fixed height. A fixed height clips the moment a user increases text spacing, and that is a WCAG failure rather than a cosmetic problem.
Placeholder text is never smaller than the value it replaces. Distinguish it by colour, not size, and never let it stand in for a label.
Numeric columns need font-variant-numeric: tabular-nums. Proportional digits are different widths, so a column of figures will not align and any number that updates in place will visibly jitter. Anything counting, timing or reporting money wants tabular figures. Consider slashed-zero too where you display IDs or codes.
Inline code should be sized in em, not px. That way it tracks whatever text surrounds it. Pair it with line-height: 1 so a snippet mid-paragraph does not push the line box open. Never apply negative tracking to monospace: Carbon actually applies positive tracking at small sizes, which looks wrong on paper and is correct on screen because it separates l from 1 from I.
A big number is not a heading. Atlassian ships a separate metric ramp for stat tiles, at 28px, 24px and 16px. Headings carry tracking and leading tuned for prose, and inheriting them makes dashboard figures look soft.
Mobile is a different problem
The 16px input rule. iOS Safari zooms the viewport whenever you focus an input rendering below 16px. At 15px it fires, at 16px it does not. The usual workaround is maximum-scale=1 or user-scalable=no in the viewport meta tag, which suppresses the zoom and also disables pinch-zoom entirely, failing WCAG 1.4.4. Do not do that. Size the input at 16px.
iOS Dynamic Type. The system ships eleven text styles, and users can move all of them at once. Body is 17pt at the default setting with 22pt leading. If you hardcode point sizes instead of using the semantic styles, none of it responds. Apple's minimum is 11pt on iOS and 10pt on macOS.
Android uses sp for text and dp for everything else. Both scale with screen density, but sp additionally scales with the user's font-size preference. Using dp for text is the Android equivalent of hardcoding pixels: it looks identical on your device and ignores the user completely.
Optical sizing is handled in the font now. If your typeface has an opsz axis, font-optical-sizing: auto adjusts letterforms for size automatically, which is more accurate than any tracking table you would write by hand. Apple does this invisibly, switching from SF Pro Text to SF Pro Display at 20pt.
The accessibility rules that actually bite
1.4.4 Resize Text. Text has to reach 200 percent without losing content or function. The practical consequence is to size text in rem rather than px. There is also a specific named failure, F94, for sizing text with viewport units, because a value like font-size: 4vw cannot respond to browser text settings at all.
1.4.12 Text Spacing, which is the most misread rule in the specification. It does not require you to set line-height: 1.5. It requires your layout to survive a user forcing line height to 1.5, paragraph spacing to 2em, letter spacing to 0.12em and word spacing to 0.16em, all at once, without anything being lost. You are free to design at 1.4 leading. You are not free to build a card that clips when someone overrides it. Fixed heights and overflow: hidden are the usual culprits.
1.4.3 Contrast. 4.5:1 for normal text, dropping to 3:1 for large text. Large means 18pt, or 14pt bold, and W3C converts at 1pt equals 1.333px, so the real thresholds are about 24px at any weight or 18.5px at bold. Bold here means 700, since many auditors will not accept 600.
1.4.11 Non-text Contrast is the one people forget. Input borders and focus indicators need 3:1 against their background. A fashionable 1px hairline at 2.5:1 fails, and it fails on precisely the elements a keyboard user depends on.
Making it fluid
To interpolate a size between two viewports, the formula is:
slope = (maxSize - minSize) / (maxViewport - minViewport) intercept = minSize - (slope * minViewport) font-size: clamp(minSize, intercept_in_rem + (slope * 100)vw, maxSize)
Going from 16px at a 320px viewport to 32px at 1440px gives clamp(1rem, 0.7143rem + 1.4286vw, 2rem).
Two things to watch. Compute the slope in pixels and convert only the intercept to rem. Doing the whole calculation in rem is where a factor-of-sixteen error creeps in, and it appears in some widely cited published examples. And keep a real rem term in the preferred value, because that term is what lets the expression respond to browser text sizing. A pure vw value is the F94 failure above.
Pick 320px as your lower viewport bound, since that is the reference width in WCAG 1.4.10 Reflow, and somewhere between 1240px and 1440px as the upper. Outside that range the clamp pins, which is what you want.
For line height, do nothing extra. Unitless line height multiplies a font size that is already fluid, so the ratios follow along on their own.
Final Thoughts
A type scale is not a design decision. It is the thing that stops you making the same decision forty more times, badly, at two in the morning.
Pick the ratio, snap the leading to your grid, cap your tracking well below what the internet tells you, ship three weights, and write the whole thing down as tokens. Then spend the attention you just saved on the part that actually distinguishes the work.
The scale is scaffolding. Nobody is going to compliment you on it. They will only notice when it is missing.
If you want to skip the arithmetic, the design system toolkit generates all of this and exports it as CSS custom properties, Tailwind theme tokens, or Figma variables.
Continue Reading
More Articles
How to Choose a Product Designer for Your Startup
Hiring the wrong product designer is expensive and slow to undo. Here's how to choose the right one for your startup: what to look for, the questions to ask, and the red flags worth walking away from.
Stop Overthinking Every Design Decision: A Practical Guide for Confident UX Designers
Overthinking is one of the most expensive habits in UI/UX design. Here are eleven frameworks and mental models confident designers use to decide with structure, speed, and confidence.
FAQ
Frequently asked questions
If my H1 is 48px, what should H2 and body text be?
With a 1.272 ratio, a 48px H1 gives you roughly 37.7px for H2, 29.7px for H3, and 18.3px for body copy. The ratio matters more than the specific numbers: pick one, apply it consistently in both directions from a size you have already decided, and round the results to whole pixels.
What ratio should I use for a type scale?
For dense product UI, an effective ratio around 1.12 to 1.2 works best because you need fine steps in the 12px to 20px range. For editorial and marketing pages, 1.25 to 1.333 gives you the drama. The square root of the golden ratio, 1.272, sits between the two and has the useful property that every second step is a true 1.618 golden interval.
Why should line height get smaller as font size gets larger?
Leading exists to help the eye find the start of the next line. Large text already gives the eye plenty to track, so a generous ratio just leaves the lines floating apart. Small text needs proportionally more space. In practice body copy sits around 1.5 and display text around 1.05, with everything in between interpolated.
What is the minimum font size for mobile form inputs?
16px. iOS Safari automatically zooms the viewport when you focus an input rendering below 16px, which yanks your layout sideways. The maximum-scale=1 meta tag suppresses it but fails WCAG 1.4.4, so the correct fix is simply to size mobile inputs at 16px or larger.