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.

CI Swift Platforms Dependencies License: MIT npm

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 → ThemeKitdesign_via_figma_mcp reads 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-tripexport_figma_variables turns the token catalog
    • 32 presets into a themeable Figma Variables library (one mode per preset); import_figma_variables pulls any company's Figma file back into a live ThemeConfig that re-skins every component. One vocabulary, both directions.
  • 🪄 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., ViewBuilder slots (.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.xcstrings into your app (no per-call code), with restart-free in-app language switching. Every default is also overridable per call.
  • 📅 Calendar add-onThemeKitCalendar adds 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).