dembrandt/dembrandt-skills/modular-scale-typography
Claude Code and agent skill by dembrandt/dembrandt-skills
- community
Checking that the SKILL.md file is still in the repository.
Ratio-based type scale where every size relates to the others. Use when defining font sizes or type tokens, or when typography feels arbitrary.
Install
npx vskill@latest install dembrandt/dembrandt-skills/modular-scale-typographySKILL.md
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_typographyreturns 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`.
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.
Optical Gaps Around Caps Labels
An all-caps or descender-free label leaves the bottom of its line box empty. Equal margins above and below it measure equal in the inspector and read larger below. Measure gaps from the visible glyphs, not the box, and shave a few pixels from the lower gap until the two look equal. This is what people mean when spacing is "off" but they cannot say why.
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.
All three browsers balance; only Chrome and Safari do pretty. Neither is load-bearing. Balance stops at six lines (Chrome) or ten (Firefox), so never on body copy. Titles, toasts, tooltips, captions and wrapping buttons strand words too: add them by class to the same rule. Balance moves breaks, not the box, so cap a left-aligned heading with max-width in ch first.
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 kg, Figure 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:
- Shorten the text. Usually the right one. A slot sized for a label was not
asking for a sentence.
- Drop it a step on the scale. Supporting text beside a title belongs a step
or two below it, not at the same size.
- 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.
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewNot runA model review is scheduled for the next rescan.
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/modular-scale-typography/SKILL.md
Popularity
- 7-day trend score
- 5
Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/dembrandt/dembrandt-skills/modular-scale-typography)