3.3 — Design tokens: color, typography & spacing
Opening scenario
You inherit a 4-year-old SwiftUI codebase. You search for Color(red: and find 312 results. Same brand blue, defined 50 different ways: some Color(red: 0.0, green: 0.48, blue: 1.0), some Color(hex: "#007AFF"), some Color("BrandBlue"), some UIColor.systemBlue. The designer just shipped a new brand color. You quit your job.
Design tokens are the cure. One name (Color.brandPrimary), one source, one change point. This chapter shows you how to set up a tokens layer that scales from a 1-person side project to a 500-engineer org.
| Layer | What it is | Examples |
|---|---|---|
| Primitive tokens | Raw values | blue500 = #007AFF, space-4 = 16pt |
| Semantic tokens | Intent-named, references primitives | colorBackground = blue500, spacingCardPadding = space-4 |
| Component tokens | Component-specific | buttonPrimaryBackground = colorBackground |
You always use semantic and component tokens. Primitives stay hidden inside the tokens module.
Concept → Why → How → Code
Apple’s semantic color system
Apple already gives you a complete semantic palette via UIKit / SwiftUI:
// Backgrounds — adapt to light/dark, elevated/grouped contexts
Color(.systemBackground) // Primary view background
Color(.secondarySystemBackground)
Color(.tertiarySystemBackground)
Color(.systemGroupedBackground) // For grouped lists
// Labels — text colors with built-in opacity hierarchy
Color(.label) // Primary text
Color(.secondaryLabel)
Color(.tertiaryLabel)
Color(.quaternaryLabel)
// Fills — for icon backgrounds, progress bars
Color(.systemFill)
Color(.secondarySystemFill)
// SwiftUI shortcuts (semantic, OS-aware)
.foregroundStyle(.primary)
.foregroundStyle(.secondary)
.foregroundStyle(.tertiary)
Use these. They adapt to light/dark, high contrast, and the new vibrant materials automatically. Never write Color.black for body text — write .primary.
Asset Catalog colors — the typed wrapper
For brand colors that aren’t in Apple’s palette, define them in the Asset Catalog (Assets.xcassets → New Color Set). Set Appearances: “Any, Dark” to give one value for light and one for dark. Now reference by name:
extension Color {
static let brandPrimary = Color("BrandPrimary")
static let brandSecondary = Color("BrandSecondary")
static let surfaceElevated = Color("SurfaceElevated")
}
Or even safer — use the Xcode 15+ generated symbols (set “Asset Symbols” generation to “Swift” in build settings):
// Auto-generated; you get compile-time-checked color access:
Color.brandPrimary // No string-typo runtime crash
Token layering in code
Even with Asset Catalog colors, structure them in semantic layers:
// DesignTokens/Color+Tokens.swift
extension Color {
// PRIMITIVES — never use directly outside this file
fileprivate static let _blue500 = Color("Blue500")
fileprivate static let _gray100 = Color("Gray100")
fileprivate static let _gray900 = Color("Gray900")
// SEMANTIC — use these in views
static let accent = _blue500
static let surface = _gray100
static let onSurface = _gray900
// COMPONENT — for tightly-bound use cases
static let buttonPrimaryBackground = accent
static let buttonPrimaryForeground = Color.white
static let cardBackground = surface
}
Designer changes the brand blue? Update Blue500 in Asset Catalog. Every screen updates.
Dynamic Type — typography that scales
Apple’s text styles (.body, .headline, .title2, etc.) scale with the user’s Dynamic Type setting. Always prefer them over hardcoded sizes.
// Wrong — won't scale, fails accessibility audit
Text("Hello").font(.system(size: 17))
// Right — scales with user preference
Text("Hello").font(.body)
// Right with weight override
Text("Hello").font(.body).fontWeight(.semibold)
// Right with custom font, still scaling
Text("Hello").font(.custom("Inter-Regular", size: 17, relativeTo: .body))
The full text style scale (you should memorize the names, not the sizes — sizes change with user preference):
| Style | Default size | Use for |
|---|---|---|
.largeTitle | 34 | Hero screens, onboarding |
.title | 28 | Screen titles |
.title2 | 22 | Section titles |
.title3 | 20 | Card headlines |
.headline | 17 (semibold) | List item titles, emphasized labels |
.body | 17 | Primary content |
.callout | 16 | Secondary content |
.subheadline | 15 | Subtitles |
.footnote | 13 | Captions, metadata |
.caption | 12 | Smallest readable text |
.caption2 | 11 | Microcopy (use sparingly) |
Custom fonts that respect Dynamic Type
Brand fonts (Inter, Söhne, GT America) should be wrapped in style helpers that scale:
extension Font {
static func brandBody(weight: Font.Weight = .regular) -> Font {
.custom("Inter-Regular", size: 17, relativeTo: .body)
.weight(weight)
}
static func brandHeadline() -> Font {
.custom("Inter-Semibold", size: 17, relativeTo: .headline)
}
}
// Usage
Text("Hello").font(.brandBody())
Text("Hello").font(.brandHeadline())
Note relativeTo: — this is what makes Dynamic Type work for custom fonts. Without it, your designer’s font looks great at default size and unreadable at the largest accessibility size.
Spacing — the 8pt grid
Apple’s design tradition uses an 8pt grid: every spacing value is a multiple of 8 (or sometimes 4 for fine adjustments). This produces visual rhythm and consistency.
enum Spacing {
static let xxs: CGFloat = 4
static let xs: CGFloat = 8
static let sm: CGFloat = 12
static let md: CGFloat = 16
static let lg: CGFloat = 24
static let xl: CGFloat = 32
static let xxl: CGFloat = 48
}
// Usage
VStack(spacing: Spacing.md) {
Text("Title")
Text("Body")
}
.padding(Spacing.lg)
Adopt this in code and your designer’s Figma will already align (if they’re competent, they’re also using the 8pt grid).
Generating tokens from Figma
For larger teams, manually maintaining Color+Tokens.swift and Spacing.swift is brittle. Tools that automate it:
- Style Dictionary — Amazon’s open-source token translator. Input: JSON. Output: Swift, Kotlin, CSS, anything.
- Figma Tokens / Tokens Studio plugin — exports Figma variables to JSON for Style Dictionary.
- Supernova — commercial: full Figma → multi-platform pipeline.
- Custom script — call Figma REST API → walk
variablescollection → emit Swift code.
The bare-metal flow:
Figma variables → API export → tokens.json → Style Dictionary → DesignTokens.swift → committed to repo
Run on every PR via CI, fail if tokens differ from latest Figma.
The full SwiftUI design tokens module
// DesignTokens.swift
import SwiftUI
enum DesignTokens {
enum Color {
static let surface = SwiftUI.Color("surface")
static let onSurface = SwiftUI.Color("onSurface")
static let accent = SwiftUI.Color("accent")
}
enum Spacing {
static let xs: CGFloat = 8
static let sm: CGFloat = 12
static let md: CGFloat = 16
static let lg: CGFloat = 24
}
enum Radius {
static let sm: CGFloat = 8
static let md: CGFloat = 12
static let lg: CGFloat = 16
}
enum Typography {
static let body = Font.custom("Inter-Regular", size: 17, relativeTo: .body)
static let headline = Font.custom("Inter-Semibold", size: 17, relativeTo: .headline)
}
}
// Usage
VStack(spacing: DesignTokens.Spacing.md) {
Text("Hello")
.font(DesignTokens.Typography.headline)
.foregroundStyle(DesignTokens.Color.onSurface)
}
.padding(DesignTokens.Spacing.lg)
.background(DesignTokens.Color.surface, in: RoundedRectangle(cornerRadius: DesignTokens.Radius.md))
A SwiftLint custom rule banning Color(red:, Color(hex:, Font.system(size:) enforces token usage.
In the wild
- Apple’s own apps lean almost entirely on
.primary/.secondary/.tintplus a handful of brand colors. Calculator, Stocks, Weather are all built on this minimal token set. - Airbnb’s DLS publishes a Swift package with hundreds of tokens generated from their Figma source.
- Shopify has Polaris — public design system with full token documentation; the iOS variant follows the same naming.
- Lyft open-sourced token-pipeline tooling — search GitHub for
lyft/tokens-studiostyle repos. - Linear (the project management app) uses a single semantic token system across web, iOS, macOS; it’s why the apps feel identical despite three codebases.
Common misconceptions
- “Dark mode is just inverting colors.” Wrong. Dark mode uses different values — usually a desaturated near-black background, lighter accent, and reduced contrast for non-essential text. Use Asset Catalog appearance variants, not algorithmic inversion.
- “I can use hex codes — I’ll convert them later.” “Later” never comes. Use tokens from day one.
- “Dynamic Type is for old people.” Dynamic Type is a system-wide accessibility setting that affects ~30% of iOS users (per Apple’s own talks). Fail it and your reviews drop.
- “Spacing doesn’t matter as long as it looks right.” Inconsistent spacing is the #1 reason an app feels “amateur.” The 8pt grid is cheap insurance.
- “Tokens are overkill for a small app.” For a 1-screen app, sure. For anything multi-screen, you’ll regret not having them by week three.
Seasoned engineer’s take
The first commit on any new project I start: a DesignTokens.swift file with placeholder values. Even before any screens exist. It forces the question “what’s our color palette?” immediately, and gives every subsequent screen a free pre-existing API.
Custom fonts are a liability if you don’t wrap them properly. The number of apps that ship with a custom font that doesn’t scale with Dynamic Type is huge — your app being one of the few that does will be a quiet competitive advantage in accessibility scoring.
If you’re at a company without a design system: build the tokens layer anyway, locally for your feature, and commit it. When the design system arrives in 18 months, you’ll be ahead.
TIP: Use
xcrun --sdk iphoneos --find xcassetsto verify your Asset Catalog colors parse correctly during CI — catches typos in light/dark color JSON before runtime.
WARNING: Don’t ship
.primary/.secondaryas your brand color. They are system colors that change with iOS releases. Brand colors must be defined explicitly.
Interview corner
Junior-level: “How do you handle colors in a SwiftUI app for light and dark mode?”
Asset Catalog Color Sets with “Any, Dark” appearances. Reference by typed extension on Color so they’re compile-time safe. Use .primary/.secondary for text where possible.
Mid-level: “What’s a design token? Why use one instead of hex codes?”
A semantic name for a design value, decoupled from its raw representation. Tokens let designers change values in one place, prevent inconsistency, and bridge multiple platforms (iOS + Android + web). Hex codes scatter and drift.
Senior-level: “Design a tokens system that syncs from Figma to iOS, Android, and web with one source of truth.”
Figma variables as source. Tokens Studio plugin exports to JSON in a shared repo. Style Dictionary transforms JSON to Swift (extensions on Color/Font/enums), Kotlin (object), CSS variables, in one CI run. PR check ensures token JSON matches Figma export. Versioned tokens module as a Swift Package consumed by the iOS app.
Red flag in candidates: Hardcoded hex codes in their portfolio app’s view code. Tells you they’ve never maintained a real product.
Lab preview
You’ll define a semantic palette from a brand brief in Lab 3.3 — Palette from Brief.
Next: 3.4 — SF Symbols