modular-scale-typography

Typography feels cohesive and intentional when font sizes follow a modular scale — a ratio-based sequence where every size is mathematically related to the others. Use when defining type scales, setting up design tokens, reviewing font size choices, or when typography feels inconsistent or arbitrary

By dembrandt · 653 installs

npx skills add dembrandt/dembrandt-skills --skill modular-scale-typography

Source repository · Upstream listing

Modular Scale Typography Typography feels cohesive when all font sizes are related to each other through a single mathematical ratio. Without a scale, sizes get picked arbitrarily and the result feels visually noisy — headings that don't contrast enough, body text too close in size to captions, labels that blend into content. What Is a Modular Scale A modular scale starts from a base size and multiplies or divides by a ratio to generate every size in the system. Every size is thus a deliberate step away from the base — not a guess. Choosing a Ratio Ratio Name Feel Good for 1.067 Minor Second Very tight Dense data UIs, dashboards 1.125 Major Second Subtle Long form reading, editorial 1.200 Minor Third Balanced Most UI applications 1.250 Major Third Clear hierarchy Marketing, landing pages 1.333 Perfect Fourth Strong contrast Display, hero sections 1.414 Augmented Fourth Dramatic Portfolios, branding 1.500 Perfect Fifth Very dramatic Use sparingly Default recommendation: 1.25 (Major Third) — enough contrast between steps to feel intentional without being theatrical. Generating a Scale Recover an existing scale, don't reverse engineer it by hand (dembrandt engine, optional). If a brand already has type on the web, get typography returns the real font sizes, weights, and line heights computed off the live DOM — infer the underlying ratio from those, then regularise it with the method below, instead of guessing which sizes were intended. See [ extract design ](../extract design/SKILL.md). Starting from base = 16px , ratio 1.25 : Step Formula Value Rounded Role 2 16 ÷ 1.25² 10.24px 10px Caption, label xs 1 16 ÷ 1.25 12.80px 13px Label, small 0 16 16px 16px Body (base) +1 16 × 1.25 20px 20px Body lg, lead +2 16 × 1.25² 25px 25px H4 +3 16 × 1.25³ 31.25px 31px H3 +4 16 × 1.25⁴ 39.06px 39px H2 +5 16 × 1.25⁵ 48.83px 49px H1 +6 16 × 1.25⁶ 61.04px 61px Display Round to whole pixels or rem — the ratio provides the intent, exact pixel rounding is fine. Design Tokens (CSS custom properties) Tailwind Config Why This Makes Typography Feel Cohesive Without a scale, designers and developers pick sizes by eye or habit ( 14px , 16px , 18px , 24px , 32px , 48px ). These feel subtly wrong because the intervals are uneven — the jump from 14→16 is small, 32→48 is large, and there is no underlying logic tying them together. The eye senses the inconsistency even when the viewer cannot name it. With a modular scale, every size step carries the same visual weight of change. Hierarchy reads clearly because each level is a proportional step away from the next, not an arbitrary gap. Minimum Font Size Body text base: 16px minimum. This is the browser default for good reason — it is the threshold below which reading comfort drops significantly, especially on screens. 16px — standard body text, the default base 14px — acceptable for secondary UI text (labels, captions, metadata) used sparingly Below 14px — do not use. Even at high DPI, sub 14px text fails WCAG contrast requirements for normal text and creates accessibility issues. In the modular scale, this means the base ( step 0 ) should be 16px, and negative steps (step 1, step 2) should be used only for genuinely secondary content — never for body copy or primary labels. The 1% heuristic. Sub 16px text is the rare exception (target: under ~1% of a page, never below 14px). Nobody counts characters, so apply it as a role test: May go below 16px — a fixed whitelist of glanced at roles: timestamps, captions, table metadata, helper text, fine print, badge labels. Never — body copy, primary labels, list item titles, anything actually read to do the task. Check: scan one screen, count distinct sub 16px roles. Two or three from the whitelist is healthy; five or more — or any reading content — means you've over shrunk. That's layout density, not a type problem: cut what's shown rather than shrink the type to fit. Type Rendering Details Size and ratio set the structure; these details determine whether the type actually reads well on screen. Letter Spacing Body and default text: keep tracking at 0 . Adding it to running text slows reading and makes the type feel loose. Uppercase and small labels: the one place tracking helps, since capitals are visually tight. Cap it at 0.04em , and reach for that only when the label genuinely needs air. Pattern: zero on lowercase body, at most a hair ( ≤ 0.04em ) on uppercase labels — never a blanket value. Over tracking reads as dated, not premium. Weight on Dark Backgrounds Light text on dark appears optically thinner — a halation effect where bright type bleeds into the dark field. Step up one weight to compensate: where regular (400) works on light, use medium or semibold for the equivalent text on dark . This keeps perceived weight consistent across modes instead of dark mode text looking frail. Monospace Monospace is for technical content where character alignment matters — code, IDs, numeric tables, diffs. Don't use it as a default UI typeface, and avoid monospace + uppercase (even widths plus tall capitals are hard to scan). Numbers That Change in Place A counter, a countdown, a price that updates, a column of figures — proportional digits give each number a different width, so the text jitters sideways on every tick and columns fail to line up. Switch to tabular figures anywhere digits update or stack; leave body text proportional, where tabular digits look gappy. Where Lines Break Headings are the place to control the break. text wrap: balance evens the lines of a short heading and stops the one word last line; text wrap: pretty only prevents the orphan and is the cheaper choice for paragraphs. Balance is capped at a handful of lines by the browser, so it is a heading tool, not a body tool. Set it once, in the stylesheet, for every element of that kind. A text balance utility sprinkled on one component holds for that component and silently stops holding on the next page someone builds. Chrome and Safari implement both. Firefox ignores them and falls back to normal wrapping, which is why neither is allowed to be load bearing. Never break a line by hand with <br to fix a widow. It is correct at exactly one viewport width and wrong at every other. Per component wrapping is a deliberate exception a designer asks for, not a default you reach for. Bind a word to what follows it with a non breaking space — 10&nbsp;kg , Figure&nbsp;3 , a name and its title. A unit stranded alone on the next line reads as a typo. Text That Does Not Fit Its Slot text wrap: balance fixes a heading that breaks awkwardly. It does not fix text that is simply too long for the space it was put in, and reaching for it there hides the real problem for one viewport width and returns at the next. When a line orphans a word, or a caption wraps to three lines beside a title, there are exactly three honest fixes: 1. Shorten the text. Usually the right one. A slot sized for a label was not asking for a sentence. 2. Drop it a step on the scale. Supporting text beside a title belongs a step or two below it, not at the same size. 3. Change the parent layout. When the text is already as short as it can say what it means, the container is the problem. Fewer grid columns at that breakpoint, a wider column, its own line, a different position. Six tiles across a 1280px viewport leaves each about 190px, and no rewording makes a number, a label and a caption fit that; three across at the same width does. The order matters. Reach for the layout when the first two have run out, not before: widening a container to rescue a sentence that should have been three words moves the same problem to the next breakpoint. What is not a fix: leaving it, or nudging the container until it happens to fit the width you are looking at. A legend is not a sentence. "Bar height is the total, the filled part is how many completed" is thirteen words in a header sized for three. It wraps, and it strands a word on the last line. Two swatches and one word each carry the same meaning and cannot wrap badly: The same applies to axis labels, table headers, chip text, and any caption sitting next to something larger. If the explanation genuinely needs a sentence, it belongs under the element rather than beside it. Two Lines in One Slot A label with an explanation under it is two jobs, and they have to look like two jobs. Given the same size, weight and colour they merge into one block of grey that reads as neither: Separate them by weight and ink, not by size — this is the exception to "different jobs step apart" above: both lines already sit near the 14px floor, so dropping the second a step pushes it under the minimum, and two steps apart is barely visible at these sizes anyway. Semibold primary over medium muted is enough, and the muted colour needs a floor of its own: pair the palette's faintest legible colour with the lightest weight (400) and it reads as illegible even where it passes WCAG contrast on colour alone, because contrast math is colour only and ignores stroke weight. Give any muted/secondary text token a minimum weight of 500 wherever it is used, not just here. Do not truncate an explanation. A name survives clipping because a reader recognises it from its first characters. A sentence does not: what is cut is the part that carried the meaning, and moving it into a title attribute hides it from touch, from keyboards, and from anyone who does not think to hover — this is why the title attribute escape hatch under Line Clamping below is scoped to supporting descriptions the user does not need to act on, never to an explanation that carries the meaning of what it is attached to. Truncate identifiers. Let explanations wrap, shorten them, or give the column more room. Type Scale by Page Context Landing pages and marketing surfaces benefit from large, expressive type — steps +4 to +6 for headlines create drama and brand presence. Feature pages and application UI should use a more controlled range — steps +2 to +3 for headings, with body text at step 0. Oversized headings inside a functional UI distract from the content and make the layout feel unbalanced. Match the ratio and scale usage to the purpose of the surface, not just the brand. Heading Hierarchy and Page Complexity A successful heading scale uses more than just font size to distinguish levels. It also respects the cognitive limits of the page. Tools for Differentiation If headings only differ by small increments of size, they become hard to distinguish at a glance. Use these tools to create a more meaningful scale: Capitalization: Use uppercase ( text transform: uppercase ) for small, lower level headings (H4–H5) to give them visual weight without needing large sizes. Letter Spacing: When using uppercase or bold headings, add at most a hair of letter spacing ( ≤ 0.04em ) — and only when the heading genuinely needs the air. Keep it subtle; over tracking reads as dated, not premium. Color: Use your brand primary colour or a slightly muted grey for secondary headings to differentiate them from the main black/dark grey text. Style: Use italics or subtle underlines for supplementary or metadata style headings. The Rule of Three (H1–H3) Most well designed pages require only three levels of heading hierarchy (H1, H2, H3) . Simplicity: H1–H3 is enough to cover the page title, section titles, and sub sections. H4–H6 is a complexity smell, not a typography problem. If you find yourself reaching for H4, H5, or H6, the page is trying to do too much. Adding smaller heading levels only hide