At a Glance
The whole method is one pipeline. Each stage feeds the next; a break at "Test" loops back to "Rebind," never to a font veto.
And the numbers behind every swap — if you remember nothing else, remember these.
1 Intent & Acceptance Criteria
Write down why you're swapping and what "done" means before comparing anything. The intent decides which metrics are pass/fail gates versus nice-to-haves.
The hard gates — any one failing disqualifies a candidate before comparison even starts.
| Reason to swap | The hard gate it sets |
|---|---|
| Language / glyph coverage | Candidate must cover the full product glyph set — ₱ currency, Filipino diacritics, symbols. A missing glyph is an instant fail. |
| Licensing / cost | License must permit app-bundle embedding on iOS and Android, plus web. No embed right = can't ship. |
| Brand refresh | New face must keep the same weight axis range so weight tokens still map. Aesthetic is the goal, but metrics still gate it. |
| Native availability | Font files must be obtainable in the formats each platform needs (variable TTF/OTF for native, WOFF2 for web). |
| Performance | File size / axis count budget — a swap that doubles the bundle for one axis you don't use is a regression. |
2 The Swap Architecture — where fonts actually bind
Understanding where a font is bound is the whole reason the swap is cheap in this system. There is exactly one intended binding layer.
Rebind the family once on the style; every layer inherits. Leaks are the only layers that don't.
Primary/Label/Large, Display/Heading…). The style holds the font family. Rebind the family on the style and every layer inherits. This is the swap.wght) and any optical-size (opsz) axes are set once. A variable font means a weight change is a number, not a new font file — so re-mapping weights after a swap is a config edit.3 Font Comparison — measure, don't eyeball
This is the step you already do — here it's made objective. Compare the candidate against the incumbent on measurable metrics, not on gut feel. The goal is a delta sheet that predicts exactly where retuning will be needed.
Same 20px, same string — the candidate's taller x-height makes it read larger and run wider. That single difference is what forces line-height and fit retuning downstream.
Rendered width of the identical string. A wider footprint is what pushes labels to wrap on narrow screens.
The exact same List Item component. The metric delta from the comparison above is what breaks the row.
| Metric | Why it matters | How to read the delta |
|---|---|---|
| x-height / cap-height ratio | The #1 driver of "looks bigger/smaller at the same px." | Higher x-height → text reads larger and wider. Retune size or line-height to match optical size. |
| Advance widths (avg char width) | Wider glyphs = more wraps on narrow mobile screens. | Measure the same paragraph's total width. > ±2% = expect reflow on tight layouts. |
| Vertical metrics (asc / desc / line gap) | Controls how line-height renders; mismatch shifts baselines in fixed-height controls. | Different metrics → buttons, chips, inputs need line-height retuning to keep the label centered. |
| Variable axes available | Determines whether weight/optical tokens survive the swap. | Missing wght range or opsz = tokens break. Same axes = clean map. |
| Weight mapping | Numeric weights aren't standardized across families. | Their 600 may read like your 500. Map by appearance, not by number. |
| Glyph / language coverage | Missing glyphs break real content. | Test ₱, diacritics, and any symbol the product uses. Any gap = fail against the intent gate. |
| Licensing / embedding | Native ships the actual font files. | Confirm embed rights for iOS bundle + Android assets + web. No right = can't ship. |
| Small-size rendering / hinting | Body text at 12–14px is where weak fonts fall apart. | Render captions and fine print on-device. Fuzzy or uneven = fail for a UI face. |
4 Metric-Compatibility Scorecard
Turn the comparison into a go / no-go. Score each metric — a fail on a hard gate stops the swap; partials become the retuning to-do list.
| Rating | Meaning | What it triggers |
|---|---|---|
| Pass | Metric matches the incumbent within tolerance. | Nothing. Inherits cleanly. |
| Partial | Close, but off enough to shift layout. | Add to the line-height / tracking retune list (Section 5). |
| Fail | Breaks a hard gate — missing glyph, no embed right, no matching weight axis. | Stop. The candidate is disqualified unless the gate can be removed. |
5 Rebind & Retune
The swap most people get wrong: they change the family and keep the old line-heights. Everything then drifts. Rebinding and retuning are one step, done on a copy.
Same 50px button. Goal: at the style layer, the new font occupies the same box as the old one.
wght axis token so each style reads like it did before — match by appearance, not by number. The old Semibold might be a new 560.6 Screen Regression Testing
Your "test in screens" step, made systematic. Don't spot-check convenient screens — test a fixed stress corpus chosen to expose reflow and baseline shift.
Same screen, same components — the swap's metric deltas surface as a wrapped amount and a clipped CTA. Both are fixed by retuning (Section 5), not by rejecting the font.
| Corpus category | Why it stresses the font | What to watch |
|---|---|---|
| Density extremes | Longest strings in the tightest layouts — table rows, list rows, chips. | Truncation, overflow, uneven wrapping. |
| Fixed-height controls | Buttons, inputs, tabs, badges have no room to grow. | Baseline shift — label no longer vertically centered. |
| Multi-line / wrapping | Cards, empty states, alerts reflow when width changes. | Line count changing from 2 → 3 breaks card height. |
| Small type | Captions, helper text, legal at 12–14px. | Legibility, hinting, cramped tracking. |
| Localized strings | Longest translation of each label. | Wraps and clips that only appear in the long language. |
7 Native Handoff
East Blue tracks SwiftUI + Compose readiness, so a font swap isn't done until it ships to native. The font files, not just the Figma binding, have to move.
Info.plist (UIAppFonts), and confirm the wght axis is reachable via .fontWeight() / a custom font modifier.FontFamily with the variable axis, and verify the weight tokens map to real FontWeight values.@font-face and the --font-* custom properties. Check the docs site renders the new face in light and dark..ipa / .apk.8 Version & Rollout
A font swap is a breaking typography change. Version it, tie it to the criteria it touches, and roll it out where it can be caught.
Merge order — always downstream. Never rebind screens ahead of the styles they inherit from.
| Action | Detail |
|---|---|
| Changelog entry | Semver-bump the library. Note old → new family, and link the criteria touched — C3 · Token Coverage (weight tokens), C4 · Native Mappability (font availability). |
| Merge order | Merge the retuned styles first, then let component libraries inherit, then screens. Never rebind screens ahead of the styles they depend on. |
| Stale-content sweep | After merge, re-check any doc that names the old font by name — Screen Design Guide typography section, component Style tabs, foundations tokens. |
| Rollback plan | Because the swap is one binding layer, rollback is re-pointing the styles to the old family. Keep the pre-swap branch until the new face is confirmed on-device. |