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.

LayerWhat it isExamples
Primitive tokensRaw valuesblue500 = #007AFF, space-4 = 16pt
Semantic tokensIntent-named, references primitivescolorBackground = blue500, spacingCardPadding = space-4
Component tokensComponent-specificbuttonPrimaryBackground = 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):

StyleDefault sizeUse for
.largeTitle34Hero screens, onboarding
.title28Screen titles
.title222Section titles
.title320Card headlines
.headline17 (semibold)List item titles, emphasized labels
.body17Primary content
.callout16Secondary content
.subheadline15Subtitles
.footnote13Captions, metadata
.caption12Smallest readable text
.caption211Microcopy (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 variables collection → 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/.tint plus 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-studio style 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

  1. “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.
  2. “I can use hex codes — I’ll convert them later.” “Later” never comes. Use tokens from day one.
  3. “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.
  4. “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.
  5. “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 xcassets to verify your Asset Catalog colors parse correctly during CI — catches typos in light/dark color JSON before runtime.

WARNING: Don’t ship .primary/.secondary as 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