04
Product
16
Backend
09
Auth
12
iOS
07
Infra
02
Real-Time

Adopt three-tier design tokens in Swift

ADR-0014 ACCEPTED · 2025-07-15
Adopt design token system in Swift

Context

SwiftUI apps accumulate hardcoded colors, spacing values, and font sizes across components. Without a system, design changes mean hunting through multiple files, dark mode support is inconsistent, and there's no semantic meaning attached to values.

We considered JSON-based design token tooling (Style Dictionary, SwiftTokenGen) but the Swift ecosystem for these is immature and poorly maintained. SwiftUI's environment system provides a natural distribution mechanism without external dependencies.

Decision

Implement a two-tier design token system in pure Swift.

Layer 1: Semantic tokens (purpose-driven values)

Purpose-driven names that map to platform-adaptive system colors, typography, spacing, border radius, and shadows:

SemanticTokens.Colors.surfacePrimary    // adapts to iOS/macOS, light/dark
SemanticTokens.Colors.brandPrimary      // brand color
SemanticTokens.Spacing.componentPaddingMD  // 16pt
SemanticTokens.Typography.headingMedium    // system font, 24pt semibold

Platform adaptation uses conditional compilation:

#if canImport(UIKit)
public static let surfacePrimary = Color(uiColor: .systemBackground)
#elseif canImport(AppKit)
public static let surfacePrimary = Color(nsColor: .windowBackgroundColor)
#endif

Layer 2: Component tokens (pre-configured styles)

Component-specific values that reference semantic tokens:

ComponentTokens.Button.Primary.backgroundColor = SemanticTokens.Colors.brandPrimary
ComponentTokens.Card.borderRadius = SemanticTokens.BorderRadius.xlarge
ComponentTokens.Input.borderColorError = SemanticTokens.Colors.intentDanger

The initial version was a flat collection of named colors and values. We restructured it into a two-tier semantic system — the three-tier plan from the original ADR dropped the raw values layer because semantic names are self-documenting and the extra indirection wasn't worth it. A primitives layer can be added later if the same raw value shows up in too many semantic tokens.

Consequences

Design changes propagate from one place. Dark mode and platform adaptation are handled at the semantic layer. Components get consistent styling without repeating design decisions.

The system can grow complex if tokens proliferate without curation. New tokens should have clear semantic purpose — if you're reaching for a name like spacing7, it's probably not semantic enough.