| name | typography-system |
| description | Type scale, line height, letter spacing, dynamic type, and font loading for a cross-platform mobile design system. Use this when defining or revising the typography layer. |
Typography System
Instructions
A mobile typography system encodes a finite, named type scale as tokens, supports platform dynamic type, and ships with predictable loading behavior.
1. Define a Named Scale, Not Ad-Hoc Sizes
Pick role-based names, not size numbers. Sizes change; roles do not.
| Role | Size | Line Height | Weight | Letter Spacing |
|---|
display.lg | 40 | 48 | 700 | -0.02em |
display.md | 32 | 40 | 700 | -0.015em |
heading.lg | 24 | 32 | 600 | -0.01em |
heading.md | 20 | 28 | 600 | 0 |
title.md | 16 | 24 | 600 | 0 |
body.lg | 16 | 24 | 400 | 0 |
body.md | 14 | 20 | 400 | 0 |
label.md | 12 | 16 | 500 | 0.02em |
caption | 11 | 14 | 400 | 0.03em |
2. Encode as Composite DTCG Tokens
Use $type: "typography" so one token carries family, size, weight, line height, and tracking.
{
"text": {
"$type": "typography",
"body": {
"md": {
"$value": {
"fontFamily": "{font.family.sans}",
"fontSize": "{font.size.14}",
"fontWeight": "{font.weight.regular}",
"lineHeight": "{font.lineHeight.20}",
"letterSpacing": "0"
}
}
}
}
}
3. Line Height Rules
- Tight headings: line height 1.1–1.25× of size.
- Comfortable body: 1.4–1.5×.
- Dense UI labels: 1.2–1.3×.
- Lock line heights to an 8pt rhythm where possible (16 → 24, 14 → 20, 12 → 16).
4. Dynamic Type / Scaling
Mobile users can increase system font size. Do not defeat it.
SwiftUI: use Font.system(.body, design: .default) or register custom fonts with Font.custom("Inter", size: 14, relativeTo: .body) so they scale.
Text("Save")
.font(.custom("Inter", size: 14, relativeTo: .body))
.fontWeight(.semibold)
Jetpack Compose: use sp (not dp) and expose scales via a Typography object.
val AppTypography = Typography(
bodyMedium = TextStyle(
fontFamily = InterFamily,
fontSize = 14.sp,
lineHeight = 20.sp,
fontWeight = FontWeight.Normal,
),
)
Flutter: TextTheme with TextStyle whose fontSize is in logical pixels; respect MediaQuery.textScaler.
final textTheme = TextTheme(
bodyMedium: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
height: 20 / 14,
fontWeight: FontWeight.w400,
),
);
React Native: wrap Text with a Typography component that multiplies by PixelRatio.getFontScale() only if you need custom scaling; otherwise trust RN's default.
5. Font Loading
- Bundle variable fonts (Inter, SF Pro fallback, Roboto Flex) over many static weights.
- Preload on cold start; never let first-paint fall back to a system font that shifts layout.
- On Android, register
FontFamily.from(Font(R.font.inter_variable, ...)). On iOS, add to Info.plist UIAppFonts and load via UIFont. On Flutter, declare in pubspec.yaml fonts: with style/weight entries.
6. Line Length and Layout
- Cap body text at ~70 characters per line even on tablet. Use
maxWidth layout constraints, not font sizing.
- Truncate with ellipsis only as a last resort; allow wrap on small screens.
7. Anti-Patterns
- Exposing raw sizes (
fontSize: 13.5) in components.
- Disabling dynamic type globally.
- Using color alone to distinguish hierarchy (fails a11y).
- Mixing
sp and dp for text on Android.
- Hardcoding
letterSpacing per screen.
Checklist