Typography
Root does not invent one scale for every platform. iOS aliases the system text styles so Dynamic Type still works. Web uses three Inter sizes. Personality comes from family, a narrow weight band, and color.
Why alias, not invent
On iOS the type scale is not only visual. The system styles (.body, .largeTitle, and the rest) are what connect to Dynamic Type. Dynamic Type is what lets a person with low vision use the app.
A custom scale next to the system scale means every size must be rescaled by hand. Someone will forget. Some text will not grow. That is not a foundation. So the scale is the system scale, and the four decisions below carry the character.
Web has no Dynamic Type contract. It does not copy the iOS ladder. Web uses three sizes. Rank on the web comes from color, then weight.
Two layers
Atomic tokens exist so a value is not written twice. Composite styles are what you use for almost all text. If size and line height are set apart, they drift. A composite style cannot drift.
- Atomic tokens. Family, size in points, line height in points, and weight. Use these for a rare case, such as an icon that must match a size.
- Composite styles.
body,headline,display, and the rest. Use these for text. If you write a rawfont-sizein a view, you are in an exception that needs a note, or you made a mistake.
iOS
Families
All three are system fonts. There is no download, no license file, and full support for Dynamic Type and optical sizes. They also align with SF Symbols.
SF Pro
family-sans
All UI: controls, labels, navigation, and body text.
.system(design: .default)
New York
family-serif
Screen titles, editorial moments, and empty states.
.system(design: .serif)
SF Mono
family-mono
Data, times, identifiers, and figures that you compare.
.system(design: .monospaced)
Screen titles use New York, not SF Pro. That is the cheapest way to give the system a face. Body text stays on SF Pro. People expect SF Pro in an iOS list. A long serif paragraph fights that habit for no gain.
SF Pro Rounded is out of the system. It reads as soft in a way that does not match the house. If a future app needs it, document the exception. Do not add a fourth default family.
Weight band
The system uses three weights: 400, 510, and 590. Bold (700) is only for serif display and title-1. Rank comes from contrast, then space. Size is for a heading, not for every rank. Weight is the last tool.
| Token | wght | Use |
|---|---|---|
weight-regular | 400 | Body, labels, and the default. |
weight-medium | 510 | Emphasis in a paragraph, numeric values, and the active tab. |
weight-semibold | 590 | Headline, buttons, and section headers. |
weight-bold | 700 | Only display and title-1 in serif. |
Do not call .bold() on body text. For emphasis in a paragraph, use weight-medium. If that is not enough, use text-primary against text-secondary.
Tracking
SF adjusts tracking at each size. Do not override it. Do not add tracking tokens to the semantic layer.
Two exceptions, both documented: family-mono below 13pt, where a little extra space helps figures; and Figma mockups, where the system does not apply tracking and you must add the table by hand.
Composite styles
The names are the iOS names, not translated. The map to .font(.body) is one to one. A family suffix appears only when the family is not the default.
These specimens use system stand-ins on the web: ui-sans-serif, ui-serif, and ui-monospace. On device, use SF Pro, New York, and SF Mono. Sizes are the Large Dynamic Type values, in points.
The kettle is already warm.
Screen title. One per view, and not always.
The kettle is already warm.
Editorial section header.
The kettle is already warm.
Section header.
The kettle is already warm.
Subheader and card title.
The kettle is already warm.
Emphasized row, button, and list header.
The kettle is already warm.
Default body. The style used most often.
The kettle is already warm.
Emphasis inside body text.
The kettle is already warm.
Long reading: notes, articles, a forecast paragraph.
The kettle is already warm.
Secondary text that is still content.
The kettle is already warm.
Metadata and row subtitle.
The kettle is already warm.
Footnotes and contextual help.
The kettle is already warm.
Labels and counters.
The kettle is already warm.
The minimum. Use only if there is no other option.
07:42 18°
Inline data and identifiers.
07:42 18°
Times, table figures, and timestamps.
Composition rules
- Minimum size is 11pt. That is
caption-2. If you need smaller type, the problem is density, not type. - Prefer two styles on one screen: one heading +
body. Usetext-primaryagainsttext-secondary, then weight, before you pick a new size. Three styles is the ceiling, counting the title. A page may have a heading. On iOS that heading can betitle-2. On the web the heading uses the same 17px size as body. - Quiet chrome uses Neutral 600. Disabled controls use Neutral 400. Do not pick a new size to make them recede. Active text stays stronger.
- Do not mix families on the same line. Serif and sans can share a screen. They must not share a line. The only exception is mono figures inside a paragraph, and only when the figures are data.
- Left align. Do not justify.
- Reading length on iPhone is 35–50 characters. On a wide window, cap the column. Do not invent new sizes for iPad.
caption-2at 11pt is not safe withtext-tertiary. Usetext-secondaryortext-primary.- Tabular figures for any number that you compare in a column. That is
.monospacedDigit()on SF Pro, notfamily-mono. Use mono for identifiers and code.
Dynamic Type
Use the system styles. Do not hard-code a size. .font(.body) grows. .font(.system(size: 17))does not.
Space and icon sizes that sit next to text must grow too. Use @ScaledMetric relative to the text style they serve.
A layout seen only at Large is not tested. Failures show at AX5, where body goes from 17pt to 53pt and a fixed row height breaks.
Web
Inter
Web is not a copy of the iOS ladder. Inter is the only sans family. There is no New York on the web. Three sizes cover reading, compact chrome, and captions. Mono uses the system ui-monospace stack as a stand-in.
Inter
family-sans-web
All web UI: the letter, docs, titles, and controls. Web only.
ui-monospace
family-mono-web
Data, times, identifiers, and figures that you compare. System stand-in on the web.
Web weight band
Inter uses 400 for body, UI, and captions. Headings use 500. That is one hundred points above body. Do not use 700 on the web. Rank comes from Neutral 1000 against Neutral 700, then this weight step. Size is not how rank is made.
| Token | wght | Use |
|---|---|---|
weight-regular | 400 | Body, UI, captions, and the default. |
weight-heading | 500 | Titles and headings only. One hundred points above body. |
Web tracking
Do not override tracking on Inter. Leave the default. Do not add tracking tokens to the semantic layer.
Web composite styles
Three sizes. Body and heading share 17/22. Heading is Neutral 1000 at weight 500. Body, UI, and caption are Neutral 700 at weight 400. The home letter uses body (.ntc-web-body).
The kettle is already warm.
Default reading text. The style used most often.
The kettle is already warm.
Page titles and section titles.
The kettle is already warm.
Tables, toast, contrast chips, and compact chrome.
The kettle is already warm.
Kickers, swatch captions, ramp hex, and type meta.
Open questions
- Web is locked to three sizes. Do not add a fourth size to solve rank. If a screen is unclear, change color or space first.
- New York on every iOS screen title is a strong bet. Prototype the first two apps with serif and with sans before you lock it.
- There is no brand display face on iOS. System fonts have a ceiling: two house apps will look related, and they will also look like any careful iOS app. If differentiation matters more than cost, spend it on a display face for titles only. Keep SF Pro for UI.
- Wide windows are not a reason for new sizes. When they arrive, decide a maximum column width.
Source of truth for the Swift aliases: foundations/Foundations.swift.