A Swift package that brings native-feeling theming to SwiftUI. Theme tokens resolve through SwiftUI's environment system — just like built-in styles — so views use .foregroundStyle(.surface) with zero boilerplate.
ThemeKit has two parts:
- Core types — the theming backbone (
ThemeAdaptiveStyle,ThemeShapeStyle) - Code generation — a command plugin that reads your config and generates all theme files
Based on the approach described in Building a Native-Feeling Theme System in SwiftUI.
In Xcode: File → Add Package Dependencies → enter the repository URL:
https://github.com/rozd/theme-kit
Create a theme.json in your project root:
{
"$schema": "https://raw.githubusercontent.com/rozd/theme-kit/main/theme.schema.json",
"styles": {
"colors": ["surface", "onSurface", { "name": "primary", "style": "primaryColor" }],
"gradients": ["primary"],
"shadows": ["card"]
}
}Token entries are either a plain string or an object with name and style — use the object form when the token name conflicts with SwiftUI built-ins (e.g. primary).
In Xcode: right-click your project → Generate Theme Files.
This generates:
ThemeColors.swift,ThemeGradients.swift,ThemeShadows.swift— token structsShapeStyle+ThemeColors.swift, etc. — convenience extensions (shadow extensions also include instance properties for composition)Theme.swift— root container with only the categories you configuredThemeShapeStyle.swift—ShapeStyleimplementation that resolves tokens via the environmentThemeShadowedStyle.swift— composing wrapper for chaining shadows onto any style (only when shadows are configured)Environment+Theme.swift— environment integrationTheme+Defaults.swift— scaffold for default theme valuescopyWithextensions for all structs
Edit the generated Theme+Defaults.swift in your project and fill in the placeholders with your app's actual style values:
nonisolated extension Theme {
static let `default` = Theme(
colors: ThemeColors(
surface: .init(light: Color(hex: 0xF7F5EC), dark: Color(hex: 0x1A1A1A)),
primary: .init(light: Color(hex: 0x1B8188), dark: Color(hex: 0x1B8188))
),
gradients: ...
)
}@main
struct MyApp: App {
@State private var theme: Theme = .default
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.theme, theme)
}
}
}Text("Hello")
.foregroundStyle(.primaryColor)
RoundedRectangle(cornerRadius: 12)
.fill(.surface)
// Shadow composition — chain shadow tokens onto any style
RoundedRectangle(cornerRadius: 12)
.fill(.surface.card)Theme tokens are resolved lazily through SwiftUI's ShapeStyle protocol. Under the hood, ThemeShapeStyle<Style> holds a key path into the Theme and resolves the correct light/dark variant at render time using the environment's color scheme:
struct ThemeShapeStyle<Style: ShapeStyle>: ShapeStyle, Sendable {
let keyPath: KeyPath<Theme, ThemeAdaptiveStyle<Style>>
func resolve(in environment: EnvironmentValues) -> some ShapeStyle {
environment.theme[keyPath: keyPath].resolved(in: environment)
}
}This means theme-aware styles work exactly like SwiftUI's built-in .primary or .tint — no @Environment property wrappers needed in your views.
Themes support immutable updates via copyWith:
theme = theme.copyWith(
colors: theme.colors.copyWith(
primary: .init(light: .purple, dark: .indigo)
)
)All theme types conform to Codable, so themes can be loaded from JSON, a remote database, or any other source.
MIT