If you've built white-label apps, tenant-branded apps or even just a SwiftUI rewrite of a UIKit app, you know the pain: colours hard-coded in three places, buttons that look slightly different on every screen, and a dark mode that only half works.
DesignSystemKit is my answer: a native design system for SwiftUI and UIKit, distributed with Swift Package Manager. One set of tokens, typography, components and icons, with themes you can switch at runtime for white-label apps, tenant branding and seasonal skins. Free, MIT licensed, and no third-party dependencies.
What's inside
| Module | What it gives you |
|---|---|
DesignSystemCore |
Semantic colour tokens (light + dark), spacing, radius, border, elevation and motion scales, typography, DSTheme, ThemeStore with persistence, a tenant ThemeRegistry, a ThemeValidator that checks WCAG contrast, and field validation. No UI framework imports. |
DesignSystemAssets |
Bundled Poppins fonts, a semantic icon catalog with SF Symbol fallback, and localized component strings (English, Spanish, German, Arabic, Hindi) with plurals. |
DesignSystemSwiftUI |
.appTheme(...), @ThemeTokens, Dynamic Type text styles and components including buttons, text fields, checkbox, radio, toggle, picker, date field, card, badge, chip, banner, progress, dialog, empty state, skeleton/shimmer and more. |
DesignSystemUIKit |
Dynamic UIColors and Dynamic Type UIFonts, theme observation for view controllers, window appearance binding, DSUIButton, DSUITextField, DSUICardView, DSUIBadge, DSUILabel. |
DesignSystemPreview |
A browsable catalog of every token and component with a live theme switcher. |
Requirements: iOS 16+, iPadOS 16+, Mac Catalyst 16+, macOS 13+ (Core, Assets and SwiftUI). Xcode 16 or later, Swift 6 toolchain.
Install
.package(url: "https://github.com/eeshuu/DesignSystemKit.git", from: "1.0.0"),
// …
.target(name: "App", dependencies: [
.product(name: "DesignSystemSwiftUI", package: "DesignSystemKit"),
.product(name: "DesignSystemUIKit", package: "DesignSystemKit"), // only if you use UIKit
]),
Fonts, icons and translations ship with the package. Nothing has to be copied into your app.
SwiftUI in a few lines
@main
struct MyApp: App {
@StateObject private var store = ThemeStore(persistence: UserDefaultsThemePersistence())
init() { FontRegistry.registerBundledFonts() }
var body: some Scene {
WindowGroup { ContentView().appTheme(store: store) } // once, at the root
}
}
struct ContentView: View {
@ThemeTokens private var ds
@State private var email = ""
var body: some View {
VStack(alignment: .leading, spacing: ds.spacing.lg) {
DSText("Welcome back", style: .title)
DSTextField("Email", text: $email, kind: .email,
validation: FieldValidator([.email("Enter a valid email")], isOptional: true).validate(email),
isRequired: true)
DSButton("Continue", trailingIcon: .arrowRight, fullWidth: true) { }
}
.padding(ds.spacing.xl)
.background(ds.colors.background)
}
}
UIKit gets the same tokens
final class ProfileViewController: UIViewController, DSThemeable {
private let store: ThemeStore
private var subscription: DSThemeSubscription?
private let titleLabel = DSUILabel(style: .heading1, text: "Profile")
private let save = DSUIButton(title: "Save", variant: .primary)
override func viewDidLoad() {
super.viewDidLoad()
subscription = store.observe(self) // applies now and on every theme change
}
func apply(theme: DSTheme) {
view.backgroundColor = .ds(.background, theme: theme)
titleLabel.apply(theme: theme)
save.apply(theme: theme)
}
}
Mixed apps work in both directions: DSHostingController themes SwiftUI inside UIKit, and the store is available via @Environment(\.dsThemeStore) for UIKit inside SwiftUI.
Runtime theming and white-label
store.selectTheme(id: "forest") // brand theme
store.setAppearance(.dark) // .system, .light or .dark
store.selectTheme(forTenant: "acme") // tenant mapping you registered
Four themes are built in (standard, forest, sunset and midnight). Create your own in code, or load one from JSON; anything the file leaves out falls back to the standard theme:
var brand = DSTheme.standard
brand.id = "acme"
brand.light[.primary] = "#D9480F"
brand.dark[.primary] = "#FFA94D"
brand.components.buttonShape = .pill
store.apply(brand) // validated, including WCAG contrast; refused if it has errors
{ "id": "acme", "name": "Acme", "light": { "primary": "#D9480F" }, "components": { "buttonShape": "pill" } }
That means a server can ship a tenant's branding as a small JSON file, and a bad palette with unreadable contrast gets rejected instead of shipped.
Accessibility and quality
The test suites cover token values, contrast of every built-in palette, theme JSON, persistence, font registration, localization and plurals, Dynamic Type scaling, UIKit observation, and snapshots in light and dark. The sample app's UI tests cover theme switching, tenants, form validation, accessibility identifiers, mixed navigation and the largest Dynamic Type size.
The repo includes a sample app (Examples/DesignSystemDemo) with a component catalog, a SwiftUI sign-up screen, a UIKit profile screen and a settings tab for theme, appearance and tenant switching.
My other Swift packages
- SecureStorageKit: Keychain, Face ID and Secure Enclave storage
- AppLoggerKit: privacy-first logging with OSLog, JSON Lines, redaction and export
github.com/eeshuu/DesignSystemKit: MIT licensed (bundled Poppins fonts under the SIL Open Font License). Stars, issues and feedback are very welcome.
Top comments (0)