typography-scale
Skill2 starsChanged 2 months agoMITView on GitHub
Defines, ports and reviews algorithmic typographic scales. USE FOR: building a size ramp from one base and ratio, choosing or indexing line-height curves, adding a density axis, porting the Equinor Design System scale, emitting scale tokens, CSS, or Figma variables and text styles. DO NOT USE FOR: measuring a font or deriving an x-height correction (use typography-x-height-alignment), optical padding and control heights, choosing which typefaces to pair.
5 other projects use a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- ainurdev/mystical-assistanttheir own version, first seen Nov 8, 2026
- thomasguillot/typography-skillstheir own version, first seen Oct 6, 2026
- equinor/design-engineering-skillsedited, first seen Sep 8, 2026
- miovrag/claude-skillstheir own version, first seen Jun 12, 2026
- launchapp-dev/animus-clitheir own version, first seen Mar 15, 2026
Other versions5 projects use a version of this.
Word for word · 0
None found.
Edited · 1
- equinor/design-engineering-skills★ 399% same
Wrote their own version · 4
- launchapp-dev/animus-cli★ 39same name
- ainurdev/mystical-assistant★ 7same name
- miovrag/claude-skills★ 1same name
- thomasguillot/typography-skills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository7 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- colour-fill-tiersSkill
- css-authoringSkill
- design-system-adrSkill
- spacing-ladderSkill
- typography-weight-matchingSkill
- typography-x-height-alignmentSkill
The file
We split this skill into the rules it gives an agent
6 rules. 0 are common, 2 are distinctive, and 4 are very specific to this project.
0
Common
Most of the 5 edited versions we read keep these.
2
Distinctive
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 68
Keep both. Collapsing to one curve plus a literal ratio at the composition layer looks equivalent and is not — see references/positions.md.
- Very specific: line 103
Known trade-off, worth deciding explicitly: if the line-height curve is indexed by *step label* (xs is always n=0), then density shifts the …
- Very specific: line 119
When a display or mono face sits beside the text face, equal nominal sizes do not look equal, so the correction has to live *somewhere* — …
- Very specific: line 172
Take
size-adjustout: Keep both mechanisms and you double-correct. - Distinctive: line 200
Keep the algorithm legible in the CSS rather than only in the generator:
- Distinctive: line 269
If you simplify any of them, do it knowing the cost and write down why.
“Common” is measured: kept by at least 6 in 10 of the 5 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Typographic scale
A scale is an algorithm, not a table of numbers. Ship the formula, derive the values, and let one constant — the base size — carry density. Every value below is reproducible from two lines of arithmetic; nothing is authored twice.
1. Sizes: one base, one ratio, one snap
size(i) = round(base × r^(i / n), snap)r = 2,nsteps per octave. The scale doubles every n steps — that is the whole point of an octave-based ratio:lg→ five steps up is exactly2 × lg, at every density. Exact in the formula; the snap costs 0.5px on a few pairs, enumerated inreferences/positions.md. Arbitrary ratios (1.25, 1.333) have no such landmark, so the ends drift.iis the step index, offset so that one named step is the base. EDS uses ten stepsxs … 6xlwithlgat the base, soi = labelIndex − 3.snapis a rounding grid, not a formatting choice — see below.
/* five steps per octave, base at lg, snapped to half a pixel */
--font-size-md: round(calc(var(--_base) * pow(2, -1/5)), 0.03125rem);
--font-size-lg: var(--_base);
--font-size-xl: round(calc(var(--_base) * pow(2, 1/5)), 0.03125rem);Snap to half a pixel (0.03125rem), not to whole pixels. Whole-pixel
rounding is too coarse at the small end, where steps are closest together: it
misses the intended ratio by up to 38.9% against half-pixel's 12.1%, and puts a
+9.09% step next to a +16.67% one in the range that carries body text. Full
figures in references/positions.md. Half-pixel keeps the small
end proportional while still landing on values a human can say out loud, and
sub-pixel type renders fine — it is sub-pixel layout that causes trouble,
which is why line-heights snap differently (section 2). Note that 0.03125rem
is half a pixel only while the root is 16px: round() snaps in the unit it is
given, so changing the root changes the grid.
State the constants somewhere durable. base, n, the step offset and the
snap are the entire scale: with them every token regenerates, without them the
tokens have to be reverse-engineered.
2. Line-height: a curve indexed by step, snapped to the grid
Large text needs proportionally less leading than small text, so a single ratio is wrong at one end or the other. Use a cubic across the scale — the multiplier barely moves for the first half and falls away sharply at the top, which is an ease-in on the leading, not an ease-out:
multiplier(n) = max − (n / (N−1))³ × drop
line-height = round(fontSize × multiplier(n), 4px)Two curves, because reading and scanning want different things:
| Curve | max | drop | For |
default | 1.39 | 0.29 | Read text — prose, meant to be consumed |
compressed | 1.13 | 0.13 | Scanned text — UI labels; a wrapped label must read as one block |
Keep both. Collapsing to one curve plus a literal ratio at the composition layer
looks equivalent and is not — see references/positions.md.
The 4px grid is the invariant; the ratio is the derived value. Snapping the
result means the rendered percentages come out non-monotonic —
152, 133, 143, 150, 130, 133, 131, 129, 113, 108 — which looks like a bug and
is not. Each line box lands on the layout grid, which is the property that
composes; the ratio is just what that costs at that step. Say so in a comment,
because someone will find the table and file an issue.
Name the second curve for what it does: compressed describes leading, while
squished is Nathan Curtis's term for a spacing proportion, and reusing it
makes two unrelated ideas collide inside one expression.
3. Density is one number
compact 0.875 comfortable 1 relaxed 1.15625Density sets base. Everything else — sizes, line-heights, and any measure
derived from a font size — follows without a second table. Ship it as an axis
on the root:
[data-density='compact'] { --_base: 0.875rem; }The attribute must sit on the element that declares --_base: [data-density]
on a wrapper does not reach a --_base declared on :root.
Derive companions from the step rather than tabulating them: an icon gap of
round(fontSize × 0.618, 2px) stays in proportion at every density, where a
flat 8px only looks right at one.
Known trade-off, worth deciding explicitly: if the line-height curve is
indexed by step label (xs is always n=0), then density shifts the size
ramp underneath a fixed curve, and the same rendered px size gets different
leading depending on which label produced it:
16px: compact/xl = 20px comfortable/lg = 24px relaxed/md = 24pxIndexing by absolute size instead makes leading purely a function of size. Both are defensible. Label-indexing is the incumbent in EDS and was kept deliberately — changing it moves rendered values in a shipped product, which is a redesign, not a fix. Decide, record the choice, and do not rediscover it.
4. Building a scale for a paired family
When a display or mono face sits beside the text face, equal nominal sizes do
not look equal, so the correction has to live somewhere — either in the ramp
or in the @font-face. Which one depends on the target, and the two options
are genuinely different outputs, not two packagings of one.
typography-x-height-alignment produces the factor this section consumes.
Ask before emitting anything:
Will this be used in a CSS-only environment, or also in Figma, React Native, or other non-CSS targets?
The answer changes the output, not just the packaging.
CSS only → one scale, corrected in @font-face
Emit a single size ramp and let size-adjust correct the secondary face.
It corrects continuously, so it also fixes sizes that are not scale steps.
@font-face {
font-family: 'Equinor';
src: url('EquinorVariable-VF.woff2') format('woff2-variations');
size-adjust: 113.7288%; /* generated from the correction token, not typed */
}Both families now use --font-size-md and look the same size. Nothing else
changes.
Also Figma or React Native → one scale per family
size-adjust is a CSS @font-face descriptor. React Native never parses it and
Figma has no equivalent, so both would render the uncorrected face while CSS
looked right. Emit two ramps:
step text (reference) display (× 1.137288, snapped) line-height (shared)
md 14px 16px 20px
lg 16px 18px 24px
xl 18.5px 21px 24px
5xl 32px 36.5px 36pxFour rules make this work:
- Correct the font size only. Apply the factor to each step and re-snap to
the same grid (
round(step × correction, 0.5px)). When the reference carries anopszaxis the factor is per step, not per family — Inter's x-height falls 5.5% from opsz 14 to 32 — so sample it at each step first:typography-x-height-alignment/references/optical-size.md. - Share the line-heights. Both families use the reference ramp's line-heights at the same step: aligned faces look the same size there, so they get the same line box and the 4px rhythm holds across families.
- Take
size-adjustout. Keep both mechanisms and you double-correct. - Check the correction survives the snap. Below half a snap unit at the
smallest step it is erased there:
× 1.019345lands as 0.00% atxs/smbut +3.57% atmd. Figures inreferences/positions.md.
scripts/scale.py --correction 1.137288 --display Equinor emits this branch
with one factor (positions §8 for the per-step limit). Two numbers under one
step label is the correct outcome: display-md = 16 beside text-md = 14
means the same perceived size, reached from different nominal values. Both
designer and developer see 16 for a display step, with no hidden multiplier.
Off-scale sizes get no correction — acceptable, they are outside the system.
5. Emit: tokens first, then CSS and Figma
scripts/scale.py derives every number from the constants and emits them,
tokens first — the DTCG file is the source, and CSS and Figma are outputs
derived from it, never the other way round:
skill=.claude/skills/typography-scale # where the installed copy lives
python3 $skill/scripts/scale.py --check # fixtures still hold
python3 $skill/scripts/scale.py --out tokens # one file per density
python3 $skill/scripts/scale.py --format figma --out figma # variables + text stylesToken shape: references/token-shape.md. Figma —
density as modes, styles bound to variables, no-MCP fallback: references/figma.md.
Keep the algorithm legible in the CSS rather than only in the generator:
--font-size-xl: round(calc(var(--_base) * pow(2, 1/5)), 0.03125rem);A reader can see the scale; changing density changes one variable and the browser recomputes. Platforms that cannot evaluate expressions — Figma, React Native — get baked values from the same source, never hand-transcribed.
round() and pow() are CSS Values 4 math functions. round() is Baseline
newly, which is a question about the target, not a verdict — so ask it
rather than deciding for the reader:
Does this need to support older browsers, or only evergreen ones?
- Evergreen only — a centrally managed or auto-updating fleet, or a
browserslistthat says so. Emit the expressions. This is the whole point: the scale stays legible and density recomputes in the browser. - Older browsers in scope — emit baked values everywhere and keep the expression in a comment, so the derivation survives even though the browser never sees it.
Defaulting to baked values "to be safe" is not neutral: it discards the
readable artefact for a constraint the project may not have, silently. If no
matrix is declared and nobody answers, say which way you went. Verify status at
https://api.webstatus.dev/v1/features/round-mod-rem (caniuse has no feature
for round()) rather than recalling it — newly since 2024-05-17, checked
2026-09-04; expect widely around late 2026, which retires this question.
6. Verification discipline
A scale is a claim that N numbers all follow from four constants. Test it:
- Every emitted value equals what the formula gives — checked differently
per branch of §5. Expressions shipped: read back computed styles from a real
browser, because the thing under test is rounding, where a generator's
round()can disagree with CSSround(nearest)on exact half-way values. Baked values shipped: there is no expression for the browser to evaluate, so recompute the literals from the four constants; a readback there only proves the browser can parse a number. - The five octave exceptions are a fixture. The pairs that miss a clean 2×
are enumerated in
references/positions.md§6. A sixth means a constant or a snap moved, and fails the build. - Auditing an existing build runs in a fixed order: reproduce the preset fixture, recompute the artefacts from the constants, regenerate and diff byte-for-byte, then read values.
- Every deviation from a previous build is enumerated. Anything not on the list fails the harness. Silence is never a pass.
- Extrapolated values are flagged where they live — token, CSS, harness output. A scale extended past its evidence says so at the point of use.
- No off-scale sizes. A hard-coded
20pxis outside the system and gets none of its guarantees: grid, paired line-height, or any step correction.
The EDS preset
Asked for a scale "based on the Equinor Design System"? The constants and the
comfortable ramp as a fixture: references/eds-preset.md.
Positions this skill takes
Four choices here cost something, and a system under delivery pressure will be
tempted to simplify each: half-pixel snapping, the second line-height curve,
deriving weight and tracking per step, and ten close steps rather than six wide
ones — this is a scale for an application interface, not a page. Each rests on
a measurement, not a preference, and the file also records two limits where the
snapped output does not deliver what the formula promises:
references/positions.md.
If you simplify any of them, do it knowing the cost and write down why.
Representative requests
Four acceptance criteria — the EDS build, the compressed line-heights, the
two-family Figma push, and auditing a committed scale — plus a routing check:
references/representative-requests.md.
Related
typography-x-height-alignment— run it first when two families are involved: it measures the fonts and produces the correction factor that section 4 consumes.css-authoring— the channel-variable pattern for--_base, and the general rule for verifying browser support. Not yet published in this repository; it is the draft onorigin/skill/css-authoring.typography-weight-matching— matching weight and letter-spacing across a pair, indexed against these steps.
Provenance
From the EDS token rework in equinor/ids-meetup-oslo-26
(Equinor-internal), prepared for the Into Design Systems Oslo meetup on
9 September 2026: src/formulas.ts (the algorithms), DECISIONS.md decision 1
(indexing the curve by step label), build/css/typography.css (the emitted
scale with formulas intact) and test/deviations.ts (the enumerated-deviation
discipline in section 6), all under eds-tokens-reworked/.
Optical padding — cap height, half-leading, and landing a control on the 4px grid while keeping an honest line-height — derives from these values but is a spacing concern, and is out of scope here.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

