East Blue Design System
Font Change Methodology
How to swap a typeface across the design system without breaking screens. Because typesetting lives in named text styles and the system runs on variable fonts, the swap itself is a one-point change — the work is proving the new font is metric-compatible before it inherits everywhere. Compare on measurements, prove compatibility, rebind at the style layer, retune, then regression-test a fixed screen corpus.

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.

1
🎯Set intentgates & criteria
2
📏Compare metricsdelta sheet
3
🔍Scan leaksoff-style layers
4
🔧Rebind & retuneone swap point
5
📱Regression testfixed corpus
6
🚀Ship & versionnative handoff

And the numbers behind every swap — if you remember nothing else, remember these.

1Swap point (text styles)
8Metrics to compare
x-height#1 break driver
±2%Advance-width tolerance
12Stress screens in corpus
3Targets: Figma · iOS · Android
wghtAxis that must map 1:1
0Loose font bindings allowed
The swap is cheap; the proof is the work. A font that renders larger or wider at the same size will reflow buttons, clip labels, and shift baselines the moment it inherits. The whole method exists to catch that before it ships.

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.

🔒 Glyph coverage 🔒 Embed license 🔒 Weight axis range 🔒 File formats 🔒 Size budget

The hard gates — any one failing disqualifies a candidate before comparison even starts.

Reason to swapThe hard gate it sets
Language / glyph coverageCandidate must cover the full product glyph set — ₱ currency, Filipino diacritics, symbols. A missing glyph is an instant fail.
Licensing / costLicense must permit app-bundle embedding on iOS and Android, plus web. No embed right = can't ship.
Brand refreshNew face must keep the same weight axis range so weight tokens still map. Aesthetic is the goal, but metrics still gate it.
Native availabilityFont files must be obtainable in the formats each platform needs (variable TTF/OTF for native, WOFF2 for web).
PerformanceFile size / axis count budget — a swap that doubles the bundle for one axis you don't use is a regression.
No swap starts without a written acceptance list. "It looks nicer" is not an acceptance criterion.

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.

Text layers
hundreds
Text style
Primary/Label/*
◆ swap point
Variable axis
wght token
⚠ leak Loose bindings & hardcoded weights bypass the style — they keep the old font. Find these first.

Rebind the family once on the style; every layer inherits. Leaks are the only layers that don't.

Intended · one point
Named text styles
Every text layer references a style (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.
Intended · axis config
Variable-font axis tokens
Weight (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.
Leak · must find first
Loose font bindings
Any text layer with a font set directly instead of via a style won't inherit the swap. These are the layers that silently keep the old font. Hunt them down before rebinding.
Leak · must find first
Hardcoded weights
A layer pinned to a named weight (e.g. "Semibold") rather than a weight token can land on a weight the new family doesn't have. Route these through the axis token.
Pre-flight scan. Before any rebind, scan for off-style typography — every text layer that isn't on a named style, and every weight that isn't on a token. That list is your leak inventory. A clean system has zero; most have a handful in one-off screens.

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.

Compare fonts at the same pixel size, side by side, on the same string.
Send ₱1,250 to JuanIncumbent · x-height 0.52em
Send ₱1,250 to JuanCandidate · x-height 0.54em → reads bigger

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.

incumbentSend ₱1,250 to Juan
candidateSend ₱1,250 to Juan · +10% wider

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.

MetricWhy it mattersHow to read the delta
x-height / cap-height ratioThe #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 availableDetermines whether weight/optical tokens survive the swap.Missing wght range or opsz = tokens break. Same axes = clean map.
Weight mappingNumeric weights aren't standardized across families.Their 600 may read like your 500. Map by appearance, not by number.
Glyph / language coverageMissing glyphs break real content.Test ₱, diacritics, and any symbol the product uses. Any gap = fail against the intent gate.
Licensing / embeddingNative ships the actual font files.Confirm embed rights for iOS bundle + Android assets + web. No right = can't ship.
Small-size rendering / hintingBody 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.
Deliverable: a side-by-side spec sheet — incumbent vs candidate for all eight rows, with a "delta" column flagging where the numbers diverge. That sheet is what you carry into the rebind.

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.

RatingMeaningWhat it triggers
PassMetric matches the incumbent within tolerance.Nothing. Inherits cleanly.
PartialClose, but off enough to shift layout.Add to the line-height / tracking retune list (Section 5).
FailBreaks a hard gate — missing glyph, no embed right, no matching weight axis.Stop. The candidate is disqualified unless the gate can be removed.
✓ Do
Gate on the metrics you can't fix — glyph coverage, licensing, axis availability. These are pass/fail.
✗ Don't
Veto a font over an x-height or width delta. Those are retunable — they belong on the fix list, not the reject pile.

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.

Continue
✓ Old font — fits, centered
Continue
✗ New, raw — reads bigger, off-center
Continue
✓ Retuned — line-height absorbs the delta

Same 50px button. Goal: at the style layer, the new font occupies the same box as the old one.

Step 1
Work on a duplicate / branch
Never rebind in the live file. Duplicate the library (Figma branch) so the swap is reversible and reviewable before merge.
Step 2
Rebind the family on each text style
Change the font family at the style level — the single swap point. Do the leak layers found in Section 2 in the same pass so nothing keeps the old font.
Step 3
Re-map weights by appearance
Set the 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.
Step 4
Retune line-height & tracking to absorb the deltas
Using the Partial rows from the scorecard, adjust line-height and letter-spacing per style so a button still fits, a card still shows two lines, and headings don't reflow. This is where a taller x-height or wider advance gets neutralized.
Absorb, don't propagate. The target is: at the style layer, the new font occupies the same box as the old one. If you get that right, everything downstream inherits correct — that's the payoff of styles + variable fonts.

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.

Compare before / after side by side. Log every reflow, clip, 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 categoryWhy it stresses the fontWhat to watch
Density extremesLongest strings in the tightest layouts — table rows, list rows, chips.Truncation, overflow, uneven wrapping.
Fixed-height controlsButtons, inputs, tabs, badges have no room to grow.Baseline shift — label no longer vertically centered.
Multi-line / wrappingCards, empty states, alerts reflow when width changes.Line count changing from 2 → 3 breaks card height.
Small typeCaptions, helper text, legal at 12–14px.Legibility, hinting, cramped tracking.
Localized stringsLongest translation of each label.Wraps and clips that only appear in the long language.
✓ Do
Send a reflow back to Section 5 retuning. A broken screen usually means a style's line-height still needs work.
✗ Don't
Reject the font because one screen broke — unless it's an unfixable metric (missing glyph, no matching weight). Otherwise, retune.
Keep the corpus fixed and named — the same 12 screens every swap. A stable corpus makes two different font candidates directly comparable, and makes the next swap faster.

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.

iOS · SwiftUI
Bundle + register
Add the variable font file to the app bundle, register it in Info.plist (UIAppFonts), and confirm the wght axis is reachable via .fontWeight() / a custom font modifier.
Android · Compose
Assets + FontFamily
Drop the font into assets/resources, declare the FontFamily with the variable axis, and verify the weight tokens map to real FontWeight values.
Web · docs site
WOFF2 + font-face
Ship the WOFF2 variable file, update the @font-face and the --font-* custom properties. Check the docs site renders the new face in light and dark.
All targets
Weight-token parity
Confirm the same weight token resolves to a matching visual weight on all three platforms. A weight that exists in Figma but not natively is a handoff break.
Confirm embed licensing covers native app bundles specifically — web-only licenses are common and don't permit shipping the file inside an .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.

Retuned text styles Component libraries Screens

Merge order — always downstream. Never rebind screens ahead of the styles they inherit from.

ActionDetail
Changelog entrySemver-bump the library. Note old → new family, and link the criteria touched — C3 · Token Coverage (weight tokens), C4 · Native Mappability (font availability).
Merge orderMerge the retuned styles first, then let component libraries inherit, then screens. Never rebind screens ahead of the styles they depend on.
Stale-content sweepAfter merge, re-check any doc that names the old font by name — Screen Design Guide typography section, component Style tabs, foundations tokens.
Rollback planBecause 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.

9 Font-Swap Checklist

Step 1
Write intent + acceptance gates
Why swap, and the pass/fail metrics (glyphs, licensing, axes). No list, no swap.
Step 2
Build the comparison spec sheet
Incumbent vs candidate across all 8 metrics, with a delta column. Score each Pass / Partial / Fail.
Step 3
Scan for leaks
Every off-style text layer and hardcoded weight. Fix routing so all typography flows through styles + tokens.
Step 4
Rebind + retune on a branch
Swap the family on each style, re-map weights by appearance, retune line-height/tracking to absorb the deltas.
Step 5
Regression-test the fixed corpus
Same 12 stress screens, before/after. Log breaks; send reflows back to retuning.
Step 6
Ship to native + version it
Bundle the font on iOS/Android/web, confirm weight-token parity, changelog with criteria links, keep the rollback branch.