ThemeKit
Native, brand-neutral SwiftUI design system — 175 token-bound components that re-skin from a single accent color: light/dark, per-subtree, zero core dependencies.
Docs · API (DocC) · Wiki · npm (MCP) · Releases · Issues · Changelog
The banner above is rendered by ThemeKit itself (its own tokens + components) — the same render pipeline that paints every tile in the gallery.
A theme-driven, brand-neutral SwiftUI component library. Every color,
typography, spacing, radius and shadow is a design token resolved at runtime
from the active Theme, so the whole UI re-skins from a single accent color —
without touching component code. Components never hardcode a color — swap the
theme and everything follows.
import ThemeKit
Features
- 🎨 Figma → ThemeKit —
design_via_figma_mcpreads a design through a Figma MCP server, the LLM maps it to idiomatic ThemeKit, and the server verifies it (get_component_api/validate_code); see the Advanced — Figma & MCP section. - 🔁 Code ⇄ Figma round-trip —
export_figma_variablesturns the token catalog- 32 presets into a themeable Figma Variables library (one mode per preset);
import_figma_variablespulls any company's Figma file back into a liveThemeConfigthat re-skins every component. One vocabulary, both directions.
- 32 presets into a themeable Figma Variables library (one mode per preset);
- 🪄 Design Mode — point ThemeKit at a free-form
design.md(or a bundled style — Linear, Notion, iOS, Brutalist, Pastel) and it re-skins every component to match, via an offline heuristic parser (+ an optional LLM path). - 🤖 AI-native — a 22-tool MCP server, a Claude Code Agent skill, and an
llms.txt, so agents generate correct, token-bound UI — all from one source. - 🧩 Design tokens everywhere — colors / radius / spacing from JSON, typography /
shadows in code; one semantic name (
fg-hero,rd-sm), different values per theme. - 🌈 33 theme presets — ThemeKit's Default plus 32 ready-made color sets (cupcake, dracula, cyberpunk, nord…) inspired by daisyUI, each recoloring the whole Ant-style palette on device.
- 📸 Snapshot + render testing — every component renders to a theme-aware PNG via
ImageRenderer; the suite guards tokens, themes, validation and renders. - 204 components — Atoms / Molecules / Organisms, all token-bound.
- 🧬 Flexibility architecture — six archetype style protocols (
CardStyle,FieldStyle,ChipStyle,BarStyle,MeterStyle,ToastStyle,ListRowStyle) let you re-skin a whole component family with.cardStyle(_:)/.fieldStyle(_:)/ etc.,ViewBuilderslots (.leading{},.trailing{},.empty{}…) inject custom content, and config modifiers accept theme tokens (.spacing(SpacingKey),.cornerRadius(RadiusRole)) alongside raw values — all additive, defaults pixel-identical. - Runtime theming — a Swift token generator + a live configurator turn any
accent (or
base-100) color into a full Ant-style palette on device (no Python, no baked files). - Validation — pure, testable predicates + a SwiftUI presentation layer.
- Accessibility — Dynamic Type and Reduce Motion honored throughout.
- Localization — English-default strings via a bundled String Catalog;
translate the entire library by dropping one
ThemeKit.xcstringsinto your app (no per-call code), with restart-free in-app language switching. Every default is also overridable per call. - 📅 Calendar add-on —
ThemeKitCalendaradds a token-bound date-range picker (DateRangePicker, built on Almanac) that re-skins with the active theme; opt-in, iOS-only. - Zero-dependency core — Lottie and the Calendar are opt-in, separate products.
- DocC catalog, a demo app, and a test suite.
Requirements
| Platforms | iOS 15.6+ · macOS 14+ · Calendar add-on: iOS 17+ |
| Swift tools | 6.2 |
| Dependencies | none (core) · lottie-ios 4.4.0+ (Lottie add-on) · Almanac 0.2.0+ (Calendar add-on, iOS 17+ only) |
Installation
Swift Package Manager. In Xcode: File ▸ Add Package Dependencies… and enter
the repository URL, or add it to your Package.swift:
dependencies: [
// Core only — a plain install resolves ZERO third-party packages.
.package(url: "https://github.com/isamercan/ThemeKit.git", from: "1.3.0"),
// Opt into an add-on's dependency via package traits (Swift 6.1+):
// .package(url: "https://github.com/isamercan/ThemeKit.git", from: "1.3.0",
// traits: ["Lottie", "Calendar"]),
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "ThemeKit", package: "ThemeKit"),
// Or, for just the theme layer (tokens + @Environment(\.theme), no components):
// .product(name: "ThemeKitCore", package: "ThemeKit"),
// Only with the matching trait enabled above:
// .product(name: "ThemeKitLottie", package: "ThemeKit"),
// .product(name: "ThemeKitCalendar", package: "ThemeKit"),
]
),
]
Products & traits
The core is dependency-free at resolution time: the add-ons live behind opt-in package traits, so without a trait enabled SwiftPM never even fetches Lottie or Almanac.
| Product | Trait | Pulls | Use |
|---|---|---|---|
ThemeKit |
— (default) | nothing | the full design system — tokens and all 204 components (re-exports ThemeKitCore) |
ThemeKitCore |
— (default) | nothing | token-only theme engine: Theme, SemanticColor, @Environment(\.theme), presets, generator — no components. Adopt alone for a minimal theme layer. |
ThemeKitLottie |
Lottie |
lottie-ios |
Lottie (After Effects / JSON) animation views |
ThemeKitCalendar |
Calendar |
Almanac (→ HorizonCalendar, iOS-only) |
a token-bound date-range calendar (DateRangePicker) that re-skins with the active theme |
Quick start
Install the theme once at the root, then build with token-bound views:
@main
struct MyApp: App {
init() { Theme.shared.applyPersistedConfig() } // restore last-used theme (optional)
var body: some Scene {
WindowGroup {
ContentView().themeKit() // inject + repaint on theme change
}
}
}
struct ContentView: View {
@ThemeContext private var theme
var body: some View {
VStack(spacing: theme.spacing(.md)) {
Text("Welcome").textStyle(.headingBase)
PrimaryButton("Get started") { await signIn() }
}
.padding(theme.spacing(.base))
.background(theme.background(.bgWhite))
.cornerRadius(.base)
.themeShadow(.elevated)
}
}
Theme.shared.loadTheme(named: "oceanTheme") // runtime switch
Per-subtree theming
Theming isn't just a global switch — any Theme can be injected into a single
subtree with .theme(_:), and every component inside re-skins to it. No
Theme.shared mutation, no global state; the rest of the app keeps its theme.
let ocean = Theme(); ocean.loadTheme(named: "oceanTheme")
let grape = Theme(); grape.applyGenerated(primaryHex: "#7C3AED") // generated on-device
HStack {
BookingCard(...) // app theme
BookingCard(...).theme(ocean) // ocean — this subtree only
BookingCard(...).theme(grape) // grape — this subtree only
}
The same components, four injected themes, one screen — brand colors follow the injected theme while semantic colors (info, success…) stay consistent:
Every component reads @Environment(\.theme) (default Theme.shared), so this is
additive and backward-compatible. Try it live in the gallery's Theme Injection page.
Theme presets
ThemeKit ships 33 ready-made theme presets — its Default plus 32 color sets
inspired by daisyUI (cupcake, dracula, cyberpunk, synthwave, nord, coffee…). Each is a
ThemePreset recipe: its accent recolors the whole Ant-style palette and its
base-100 becomes the surface tone, so every theme keeps its signature look —
cupcake stays cream, cyberpunk yellow, dracula slate. The same components,
four injected themes:
Apply one live, or drop the bundled ThemePicker into any screen for a
theme switcher (it's the demo app's Themes tab):
ThemePreset.named("dracula")?.apply() // recolors Theme.shared on the fly
@State private var active: String? = "cupcake"
ThemePicker(selection: $active) // a tappable grid of all 33 themes
Import a CSS theme (HeroUI, Tailwind, shadcn…)
Already have a web design system as CSS custom properties? Hand it to ThemeKit
directly — the oklch() / hex variables are parsed on-device at runtime and
the whole token set is generated for you. No JSON, no build step. ThemeKit even
ships a ready-made HeroUI theme.
Drop a .css in your app and apply it — one line:
Theme.shared.loadTheme(cssNamed: "heroui", font: "Inter") // bundled HeroUI theme
Theme.shared.loadTheme(cssNamed: "brand") // your own brand.css in the app bundle
Or apply a CSS string — from a file, a network response, anywhere:
let css = try String(contentsOf: url) // your theme.css
Theme.shared.setTheme(css: css) // parsed + applied instantly, no restart
Theme.shared.setColorScheme(dark: true) // switches to the CSS's .dark block
Both the :root/.light and .dark blocks are read; --accent drives the
primary/info palette, --danger / --success / --warning the semantic colors,
and --background / --foreground / --border / --muted the neutral surfaces
and text (--radius / --field-radius → the box/field radius roles). Anything
the CSS doesn't define falls back to ThemeKit's defaults, and the CSS is treated
as untrusted text — only --var: value; declarations are read, nothing is executed.
Prefer to bundle a static JSON (zero runtime parse)? The same conversion runs offline as a Python tool:
# theme.css → brandTheme.json + brandThemeDark.json (light + dark)
python3 tools/import_css_theme.py theme.css --name brand \
--out Sources/ThemeKitCore/Resources --font Inter
Theme.shared.loadTheme(named: "brandTheme") // then apply like any bundled JSON theme
Screenshots
The demo app on device — the component catalog, live theming, the design-token gallery, and a full booking flow built entirely from ThemeKit components.
Real screens from the bundled Demo app, not mockups — every pixel is a ThemeKit component reading live design tokens.
Components
175 token-bound components, grouped by complexity:
- Atoms (44) —
Badge,Chip,Avatar,Icon,Rating,Spinner,StatusDot,ProgressBar,PriceTag,PointsBadge,CountdownTimer,QRCode,Barcode,Aura,TiltCard,CodeBlock… - Molecules (64) —
TextInput,OTPInput,Select,Checkbox,RangeSlider,SearchBar,TimeField,GuestSelector,PriceHistogram,InstallmentSelector,CurrencyPicker,Dropdown,ScrubGallery, buttons… - Organisms (67) —
Card,Carousel,DataTable,Accordion,Timeline,NavigationBar,Sidebar,FlightCard,FareSummary,ReviewCard,LoyaltyCard,SeatMap,LocationCard,HotelResultCard,BoardingPass,BrowserFrame,WindowFrame,PhoneFrame…
Every component is curated by category in the DocC catalog, and listed with a verified usage snippet on the docs site — Atoms · Molecules · Organisms.
Component gallery
Rendered straight from the library via ImageRenderer (plain <img> so they render everywhere, including the GitHub mobile app) —
regenerate with make screenshots. Interactive overlays (Dialog, Drawer, Tour,
BottomSheet…) and media components are best seen live in the Demo app.
Atoms
Molecules
Organisms
Overlays (animated)
Entrance previews rendered from the live components. SelectBox, BottomSheet, Tour and Feedback use OS-owned presentations (native Menu / .sheet) that no offscreen renderer can capture — record them from the running app with make record-gif NAME=SelectBox (boots the simulator, you tap to open the dropdown; see docs/SCREENSHOTS.md).
No comments yet
Be the first to share your take.