Repository navigation
Description
Overview
The original framing — re-type the App's font values to match the Figma library — is the wrong direction and re-drifts the moment Figma or the code changes again. The design-system norm at scale is the reverse: code is the source of truth, Figma mirrors it, and drift is stopped by enforcement, not by periodic re-syncing.
So the goal here is not "match the numbers once." It's to establish a typography token layer that is the single source of truth, named to match Figma's semantic styles, and make raw font sizing impossible to add. Automated Figma→code sync is explicitly out of scope — it's over-engineering for a ~12-value scale that changes rarely.
Problem
Type styles have drifted and tangled, with no mechanism to stop it:
fontSizeH1andfontSizeH2are both19;textHeadline/textHeadlineH1share a size with different line heights.- ~75
text*styles express ~12 real roles because color is baked into type styles (textNormalThemeText,textMicroSupporting, …), exploding combinatorially across color × size. - 205 raw
fontSize:and 115 rawlineHeight:literals bypass the scale entirely. This — not Figma changing — is where drift actually comes from.
Scope (zero behavior change)
- Token file (
src/styles/typography.ts) — two tiers: a primitive scale (sizes, line heights, weights, families) + semantic styles named to match Figma (body,bodyStrong,label,caption,headline,display). Type only — no color. - Alias the existing
text*styles to the new tokens, re-attaching color at the alias boundary so every current call-site renders byte-identically. <Text variant>prop keyed to the semantic styles; deprecate the free-formfontSizeprop.- Lint rule banning raw
fontSize/lineHeightoutside the token file (existing violations grandfathered viaeslint-seatbelt).
Out of scope
- No automated Figma→code sync pipeline.
- No big-bang migration of call-sites. The lint rule +
variantprop pull files onto the system as they're next edited; an optional incremental sweep (one surface per PR, with screenshots) can accelerate it later.
Example
Foundation — semantic styles compose the primitive scale, no color:
const textVariants = {
body: {...FontUtils.fontFamily.platform.EXP_NEUE, fontSize: fontScale.body, lineHeight: variables.fontSizeNormalHeight},
headline: {...headlineFont, ...whiteSpace.preWrap, fontSize: fontScale.title, lineHeight: variables.lineHeightSizeH1},
// label, caption, bodyStrong, display…
} as const;Alias — old names keep working, now sourced from the scale (color re-attached here):
// before
textNormalThemeText: {color: theme.text, fontSize: variables.fontSizeNormal},
textHeadlineH1: {...headlineFont, ...whiteSpace.preWrap, color: theme.heading, fontSize: variables.fontSizeXLarge, lineHeight: variables.lineHeightSizeH1},
// after — identical output
textNormalThemeText: {...textVariants.body, color: theme.text},
textHeadlineH1: {...textVariants.headline, color: theme.heading},Call-site — color and type decouple (FeedbackSurvey.tsx):
// before
<Text style={[styles.mt1, styles.textNormalThemeText]}>{description}</Text>
// after
<Text variant="body" color={theme.text} style={styles.mt1}>{description}</Text>Lint rule — stops the next raw literal at the door:
42:9 error Raw `fontSize: 17` is not allowed. Use a <Text variant> or a token
from src/styles/typography.ts expensify/no-raw-typography
Blast radius
- Now: ~6–8 files (token file + alias edits in
src/styles/index.ts+Text.tsx+ FontUtils map + ESLint config/rule). - Eventually (passive, never at once): ~597 files reference
styles.text*or raw font literals (~9.5% ofsrc). Aliasing means none of them need to change; they heal one line at a time as the linter flags them.
Needs design input
Design to confirm the semantic scale and the Figma-name → token map before the token file lands — the one part that can't be derived from code.
Upwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~01c1bcd6eca2caf1d6
- Upwork Job ID: 1763210051994648576
- Last Price Increase: 2024-02-29
Activity
melvin-bot commentedon Feb 29, 2024
Job added to Upwork: https://www.upwork.com/jobs/~01c1bcd6eca2caf1d6
melvin-bot commentedon Feb 29, 2024
Triggered auto assignment to Contributor Plus for review of internal employee PR - @rushatgabhane (
Internal)melvin-bot commentedon Feb 29, 2024
Triggered auto assignment to @dannymcclain (
Design), see these Stack Overflow questions for more details.melvin-bot commentedon Feb 29, 2024
Triggered auto assignment to @Christinadobrzyn (
Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details.131 remaining items
grgia commentedon Sep 1, 2026
Yes, small follow up needed. Both remaining
fontSizeH1call-sites (src/styles/index.ts:3471,MoneyReportView.tsx:135) are 19px, same asfontScale.h2, so swapping them and deletingfontSizeH1is zero visual change. we should also widen the lint rule to catchvariables.fontSize*. The unaliasedtext*` styles stay out of scope, some shift visuals so they need design sign-off first@aimane-chnaif
melvin-bot commentedon Sep 2, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Sep 2, 2026
Payment Summary
Resolving PRs:
[Typography] Add semantic type tokens and block new raw font sizes #96880
Reviewer: @aimane-chnaif owed $250 via NewDot
BugZero Checklist (@mallenexpensify)
grgia commentedon Sep 4, 2026
PR is in prod with no regressions, payment is with @mallenexpensify. @aimane-chnaif any interest in the
fontSizeH1follow up PR?aimane-chnaif commentedon Sep 4, 2026
I can work on this with Melvin
grgia commentedon Sep 4, 2026
@aimane-chnaif sure, I opened an issue and copied over the above #37503
mallenexpensify commentedon Sep 4, 2026
Payment Summary
Contributor+: @aimane-chnaif due $250 via NewDot
mallenexpensify commentedon Sep 4, 2026
I'm assuming we don't need a test case here.
garrettmknight commentedon Sep 14, 2026
$250 approved for @aimane-chnaif