11 min read

Stop hardcoding colors in SwiftUI: a practical token-based theming pattern

If you are still scattering hex codes across SwiftUI views, you are paying a hidden tax every time design changes. This post walks through a practical token-based theming pattern and shows how DesignFoundation implements it so you can switch complete themes at runtime without ripping apart your components.

You open a SwiftUI file, search for "#", and find 14 different hardcoded colors scattered through a single view. You already know this will hurt the next time design comes back with "can we soften the brand blue and add a dark theme?"

This is exactly the mess DesignFoundation is meant to avoid. It is a free, MIT-licensed, production-grade SwiftUI design system that pushes every visual decision into tokens so you can switch themes at runtime without rewriting components. If you care about having a real SwiftUI design system instead of copy-pasting modifiers around, this is the pattern underneath it.

What "token-based" actually means in SwiftUI

Forget the slideware version. In code, a token-based design system means your views never reach for a color, radius, or font directly. They ask some theme object for "primary foreground", "card radius", "xs spacing" and so on, and the theme decides what that looks like at that moment.

DesignFoundation builds that into SwiftUI instead of leaving you to reinvent it from scratch. Everything visual lives in design tokens: colors, spacing, corner radii, shadows, typography. Components you build on top accept a style or look up tokens, and they do not call Color("BrandBlue") or .cornerRadius(12) directly. That single rule is what makes runtime theme switching feel boring and safe instead of magical and fragile.

The repo is here if you want to skim the code while you read: https://github.com/NerdSnipe-Inc/design-foundation. Full API docs live at https://nerdsnipe-inc.github.io/design-foundation/, and the overview page is at https://nerdsnipe.cc/opensource-projects/design-foundation.

A concrete taste: views asking for tokens, not colors

I am not going to walk through the whole API here, but the mental model is straightforward. You build views that depend on a theme environment, not on literal values. Pseudo-code:

struct PrimaryButton: View {
@Environment(\.dfTokens) private var tokens

var title: String
var style: DFButtonStyle = .primary

var body: some View {
Text(title)
.font(tokens.typography.button.font)
.padding(tokens.spacing.md)
.background(tokens.color.button.background(for: style))
.foregroundStyle(tokens.color.button.foreground(for: style))
.cornerRadius(tokens.radius.button)
.shadow(tokens.shadow.button)
}
}

The specifics differ, but that is the pattern in DesignFoundation. Your component code describes relationships between tokens, not the final values. The theme object plugged into the environment decides what those tokens resolve to at runtime, which is what makes style swapping possible.

Why hardcoded SwiftUI theming bites you later

On a two-screen side project, hardcoded colors feel fine. You reach for .foregroundColor(.blue), ship, and move on. The problems start when any of this changes:

  • Marketing refreshes the brand palette but only for marketing pages, not the app chrome.
  • You add dark mode a year in and discover a lot of "oops, that white was in a random subview".
  • You spin up a white-label variant for a partner and need a different feel on the same flows.
  • The product grows from two developers to six and "what color should this be" starts fights in code review.

At that point, the lack of a proper SwiftUI design system is no longer an abstract quality issue. It becomes time you spend hunting down .opacity(0.4) choices that were made three months ago by someone who has since moved on.

With tokens, the decision surface is much smaller. You change a hex code or a radius in one theme object, and every component that depends on that token shifts together. You can still make bad calls, but at least those calls are centralized.

"We did not need a fancy design system, we just needed to stop arguing about colors in PRs. Once we had tokens, those conversations vanished."

- Senior iOS dev at a 12-person SaaS team in Toronto

Runtime theme switching without gymnastics

Once all of your visual decisions route through tokens, runtime theme switching becomes a one-line concern instead of an architecture rewrite. In DesignFoundation, that looks like a simple modifier on your root view:

@main
struct YourApp: App {
var body: some Scene {
WindowGroup {
RootView()
.dfThemePreset(.slate)
}
}
}

The preset is just a set of token values. The .dfThemePreset modifier wires that into the environment in a concurrency-safe way, and your components respond automatically. You can drive the preset from user defaults, a remote config, or a feature flag and get instant app-wide updates without re-render bugs.

The default behavior uses system semantic colors so your app respects iOS and macOS behavior without extra work on your side. When you outgrow that, you switch to a preset or your own custom theme that uses the same machinery.

DesignFoundation in practice: what you actually get

DesignFoundation is not a single-button "generate my UI" tool. It is a set of strongly opinionated building blocks for doing token-driven theming in SwiftUI in roughly the way you probably would have written it if you had a spare month.

Theme presets you can ship with

Out of the box, you get a few complete themes with consistent color, radius, and typography choices that are actually usable in real apps:

  • The default config uses adaptive system semantic colors. If you only call .dfThemePreset() without an argument, you get behavior that respects iOS and macOS light and dark appearance automatically.
  • .slate is a navy-slate look that reads like a clean SaaS or dev tools UI.
  • .aurora leans electric violet and rounded, aimed at creative or consumer apps.
  • .copper is warm amber on cream with sharper corners, good for finance or premium lifestyle branding.
  • .sage is deep green and very rounded, aimed at health, wellness, and similar categories.

Each preset is already wired with both light and dark variants. The theme switches with the system color scheme without you writing conditional logic or splitting colors manually. You still own your navigation structure, state, and domain logic, but the look and feel is a dropdown instead of a month of visual refactoring.

Liquid Glass and platform-specific polish

If you are targeting visionOS or newer iOS hardware, the translucent, depth-aware Liquid Glass material is where apps start to feel like they belong on those platforms. DesignFoundation has native support for that material built in, so your tokens can express "use a glassy surface here" rather than you hand-wiring platform conditional code into every view.

You treat Liquid Glass as just another part of the token vocabulary. That keeps 3D-style affordances out of your business logic and in the theme, where they belong. On platforms that do not support it, you can have the tokens fall back to a simpler material.

Concurrency-safe theming with Swift 6

One of the things I changed my mind about in 2024 was how serious to be about concurrency in UI-related state. A few months ago, I wired a custom theming layer for a 9-person fintech team in Montreal. It used a simple shared theme manager with @Published and some DispatchQueue.main juggling. It worked until someone started toggling themes from an async sign-in flow and we got delightful intermittent crashes about "Modifying state from a background thread" that never reproduced reliably in development.

DesignFoundation has been fully audited for Swift 6 strict concurrency. There are no Sendable warnings, no data races, and no "suppress this check" annotations to keep the compiler quiet. You can drive theme changes from async/await flows or actors without wondering if you just introduced undefined behavior. If you have already turned on Swift 6 language mode in Xcode 16, you know that anything touching ObservableObject and environment state quickly becomes noisy. Having this part of the stack already clean saves a lot of friction.

A simple pattern to adopt in your own components

You do not have to throw away your views to start using a token-based system. Most teams I see succeed with this migrate gradually, starting at the edges they touch most often.

Step 1: stop reaching for literal values

Next time you add a component, try this rule: no direct Color, Font, spacing literals, or raw cornerRadius inside the view body. Instead, inject what you need from your theme environment. With DesignFoundation, that usually looks like an @Environment binding to the token container, then reading from it.

Instead of:

Text("Hello")
.font(.system(size: 16, weight: .semibold))
.foregroundColor(Color("BrandBlue"))

you move toward:

@Environment(\.dfTokens) private var tokens

Text("Hello")
.font(tokens.typography.bodyStrong.font)
.foregroundStyle(tokens.color.text.primary)

Even if you never use DesignFoundation itself, this is a worthwhile discipline. It is how you keep new components from repeating the old mistakes.

Step 2: centralize a small set of tokens

Pick a thin slice of the app to start with, usually buttons or forms. Wrap up the color, spacing, and radius decisions for that slice into an obvious token structure. In DesignFoundation, that structure already exists, so you customize the preset or define your own theme instead of inventing a new one.

Once you see that actually work, it gets much easier to justify pulling the older views in line. You can do this incrementally. There is no large "big bang" migration required to get value from a token-driven setup.

Step 3: flip on runtime switching

Only after tokens are real should you worry about runtime theme switching. Until then, a theme picker is a parlor trick.

With DesignFoundation, you can wire a simple in-app toggle to let designers or product people flip between .slate and .aurora while looking at live screens:

@State private var preset: DFThemePreset = .slate

RootView()
.dfThemePreset(preset)

Your DFThemePreset value can come from user settings, an experiment framework, or per-tenant config. You do not have to special-case dark mode, because each preset already carries both light and dark variants that follow the system setting.

Where this pattern does and does not help

There are a few places where a token-based approach like DesignFoundation clearly earns its keep:

  • You are building a multi-screen app or product, not a single marketing page.
  • You expect to support both iOS and macOS, and you want coherent visuals across them.
  • You plan to ship different visual brands on the same codebase (white-label, per-tenant branding, agency work).
  • Your team cares about Swift 6 strict concurrency and wants to keep compiler warnings to a minimum.

It is less compelling if you are cranking out quick one-off utilities that will never see a second design pass. The token overhead is real, even if it is smaller than rolling your own system. You will spend some mental energy thinking about naming, structure, and where a particular visual decision should live.

One client I worked with, a three-person indie shop in Vancouver, tried to adopt a similar pattern too early and ended up with "token sprawl". Every designer they worked with invented their own slightly different notion of what "primary" meant, and the token map became more confusing than just writing colors inline. The fix was to constrain the surface area: a small, well-named set of tokens that rarely change is better than a perfectly expressive map of every possible shade.

DesignFoundation reflects that bias. It gives you a focused set of tokens rather than an infinitely flexible but unbounded graph. You can extend it, but if you find yourself adding new tokens weekly, that is a sign that your visual language, not your tooling, is the problem.

If this matches what you have been sketching on your own whiteboard for SwiftUI theming, save yourself a week or three of plumbing and start from DesignFoundation instead of a blank file. It is free, MIT-licensed, and already pulled out of real production apps that had to handle strict Swift 6 concurrency and runtime theme switching without turning into a science project.

You can install it using Swift Package Manager with .package(url: "https://github.com/NerdSnipe-Inc/design-foundation", from: "1.0.0"), or by adding the GitHub URL in Xcode under File → Add Package Dependencies. The source is at github.com/NerdSnipe-Inc/design-foundation, the docs are at nerdsnipe-inc.github.io/design-foundation, and there is a higher-level overview at nerdsnipe.cc/opensource-projects/design-foundation.

If you are wrestling with a bigger design system or theming refactor and want another pair of eyes on how to structure it, this is the kind of thing we help teams with at NerdSnipe Inc. In the meantime, drop DesignFoundation into a sandbox project, wire up .slate or .aurora on your root view, and see how it feels when your next "can we refresh the look" request is a theme tweak instead of a sprint.

Frequently Asked Questions

Ready to act on this?

Book a free 45-minute AI strategy call.

We'll look at your specific business, find the highest-value AI opportunity, and give you a clear next step — no pitch, no pressure.