# ThemeKit — Full Documentation > A token-driven, brand-neutral SwiftUI design system. **205 components** (49 atoms · > 88 molecules · 68 organisms), **34 theme presets**, **217 design tokens**, runtime theming, > and an AI-native toolchain (MCP server, agent skill, Figma round-trip). Swift 6.2 · > iOS 17+ / macOS 14+ · zero core dependencies. Companion files: **llms.txt** (index) · **llms-components.txt** (per-component API) · **llms-patterns.txt** (recipes). --- ## Overview ThemeKit is 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.** **Core principles** 1. **Token-driven** — UI code references tokens, never literals. `theme.text(.textPrimary)`, `Theme.SpacingKey.md.value`, `Theme.RadiusRole.box.value`. 2. **Brand-neutral** — ships no brand; one accent hex generates a full Ant-style palette on-device via `ThemeGenerator`, so any product adopts it. 3. **Chainable & composable** — required content/bindings/actions go in `init`; every variant, size, flag, color and callback is a copy-on-write chainable modifier. 4. **Native-first** — sizing is `.controlSize(_:)`, disabling is `.disabled(_:)`; the API reads like SwiftUI, not a bespoke DSL. 5. **AI-native** — the same machine-readable data that documents the components drives an MCP server, a Claude Code agent skill, and a Figma variables round-trip. ## Installation Swift Package Manager. Requirements: **Swift 6.2**, **iOS 17+ / macOS 14+**. ```swift // Package.swift dependencies: [ .package(url: "https://github.com/isamercan/ThemeKit.git", from: "1.0.0"), ] ``` Products (add the target dependency you need): | Product | What you get | Third-party deps | | --- | --- | --- | | `ThemeKit` | Full catalog — every component (re-exports the core) | none (core) | | `ThemeKitCore` | Token engine only: tokens + `@Environment(\.theme)` + presets + generator | none | | `ThemeKitLottie` | Optional Lottie (After Effects / JSON) animations | lottie-ios (trait) | | `ThemeKitCalendar` | Optional token-bound date-range calendar (iOS-only) | Almanac (trait) | **Traits.** The default trait set is **empty**, so a plain package reference resolves the core with **zero** dependencies. Opt into an add-on's dependency at resolution time: ```swift .package(url: "https://github.com/isamercan/ThemeKit.git", from: "1.0.0", traits: ["Lottie"]) .package(url: "https://github.com/isamercan/ThemeKit.git", from: "1.0.0", traits: ["Calendar"]) ``` ## Quick start ```swift import ThemeKit @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .environment(Theme.shared) // inject the theme once at the root } } } struct ContentView: View { @Environment(\.theme) private var theme @State private var email = "" var body: some View { VStack(spacing: Theme.SpacingKey.md.value) { Text("Sign in") .textStyle(.headingSm) .foregroundStyle(theme.text(.textPrimary)) TextInput("Email", text: $email) .icon(leading: "envelope") .a11yID("login.email") PrimaryButton("Continue") { /* … */ } .fullWidth() } .padding(Theme.SpacingKey.lg.value) .background(theme.background(.bgWhite)) } } ``` --- ## Architecture ### The `Theme` object `Theme` is an observable object holding the resolved token set. There is a shared singleton `Theme.shared`, and it is published into the SwiftUI environment under `\.theme` (default `Theme.shared`). Components read tokens from the **environment** theme, so a per-subtree theme override (`.theme(_:)`) automatically re-skins everything below it. ```swift @Environment(\.theme) private var theme // read tokens in any View theme.text(.textPrimary) // Color theme.background(.bgWhite) // Color theme.border(.borderPrimary) // Color ``` ### Token system (217 tokens across 11 groups) | Group | Count | Accessor | Example keys | | --- | --- | --- | --- | | Foreground | 10 | `theme.foreground(_:)` | `.fgHero .fgSecondary .fgTurquoise` | | Background | 24 | `theme.background(_:)` | `.bgWhite .bgHero .bgElevatorPrimary .bgSecondary` | | Border | 12 | `theme.border(_:)` | `.borderPrimary .borderHero .borderTurquoise` | | Text | 7 | `theme.text(_:)` | `.textPrimary .textSecondary .textHero .textDisabled` | | Palette | 100 | — (backing scale) | primary/secondary/accent/neutral 50→900 ladders | | Semantic colors | 12 | `SemanticColor` | `.primary .success .warning .error .accent …` | | Radius | 8 | `Theme.RadiusKey` | `.none .xs .sm .md .base .lg .xl .xl4` | | Radius roles | 3 | `Theme.RadiusRole` | `.box .field .selector` | | Spacing | 8 | `Theme.SpacingKey` | `.none .xs .sm .md .base .lg .xl .xl4` | | Typography | 34 | `TextStyle` (`.textStyle(_:)`) | Display / Heading / Label / Body / Overline / Link | | Shadow | 3 | `ShadowStyle` | `.elevated .tabBar .soft` | **Colors — the four color-key enums** - `Theme.TextColorKey`: `textPrimary, textSecondary, textTertiary, textDisabled, textHero, textPurple, textSecondaryInverse` - `Theme.BackgroundColorKey`: `bgWhite` (default component surface / base-100), `bgHero`, `bgElevatorPrimary`, `bgElevatorTertiary`, `bgSecondary`, `bgSecondaryLight`, `bgTertiary`, `bgTurquoise`, `bgTurquoiseLight`, `bgOrange`, plus badge & system tints (24 total). - `Theme.BorderColorKey`: `borderHero, borderPrimary, borderOrange, borderTurquoise` + system success/error/warning/info (12 total). - `Theme.ForegroundColorKey`: `fgHero, fgSecondary, fgTurquoise` + badge/system fg (10 total). **Semantic colors** (`SemanticColor`) — the recolorable, theme-independent palette: `primary, secondary, accent, neutral, info, success, warning, error, turquoise, orange, purple, pink`. Each exposes a **50→900 shade ladder** (`.shade(.s50)….shade(.s900)`) and variant helpers. On configurable components, pair a `SemanticColor` with a `FillVariant` (`.solid .soft .outline .ghost`) so brand/accent recolor for free: ```swift Badge("Sale").badgeStyle(.error).variant(.solid) ThemeButton("Save") { save() }.color(.accent).variant(.soft) ``` **Radius** — two ways to name a corner: - **By role** (preferred — re-rounds a whole category from one token): `Theme.RadiusRole.box.value` (cards/modals/sheets/alerts), `.field.value` (buttons/inputs/selects/tabs), `.selector.value` (checkboxes/toggles/badges). - **By size ramp**: `Theme.RadiusKey.none|xs|sm|md|base|lg|xl|xl4` (token ids `rd-xs`…`rd-4xl`). A theme that omits a role token falls back to the matching size key. **Spacing** — `Theme.SpacingKey.none|xs|sm|md|base|lg|xl|xl4` (token ids `sp-xs`…`sp-4xl`). Resolve with `.value`: `padding(Theme.SpacingKey.lg.value)`. **Typography** — `TextStyle` is a 34-style Montserrat ramp (falls back to the system font when Montserrat isn't bundled). Apply with the `.textStyle(_:)` view modifier: - Display: `displayLg displayMd displayBase displaySm` - Heading: `heading2xl headingXl headingLg headingMd headingBase headingSm headingXs heading2xs heading3xs` - Label: `labelLg600 labelLg700 labelMd600 labelMd700 labelBase600 labelBase700 labelSm600 labelSm700` - Body: `bodyLg500 bodyLg400 bodyMd500 bodyMd400 bodyBase500 bodyBase400 bodySm500 bodySm400` - Overline: `overline400 overline500` · Link: `linkMd linkBase linkSm` **Shadows** — `ShadowStyle.elevated | .tabBar | .soft`, JSON-driven per theme (so a theme switch also changes elevation feel), applied through the shared `.elevation(_:)` view modifier. ### Runtime theming `ThemeConfig` is the recipe; `ThemeGenerator` expands one accent into the full palette on-device. ```swift // 1) Recolor the whole app from ONE accent (generates the full palette): Theme.shared.applyGenerated(primaryHex: "7C3AED") // 2) Full config — accent + base surface + brand secondary/accent, light or dark: Theme.shared.apply(ThemeConfig(primaryHex: "ff79c6", baseHex: "282a36", secondaryHex: "bd93f9", accentHex: "ffb86c", dark: true)) // 3) A bundled preset: ThemePreset.named("dracula")?.apply() // applies to Theme.shared Theme.shared.apply(ThemePreset.named("nord")!.config) // 4) Scope a theme to ONE subtree only (leaves the rest of the app untouched): let ocean = Theme(); ocean.applyGenerated(primaryHex: "0FB4AB") BookingCard().theme(ocean) ``` ### Flexibility architecture — the 6 style protocols Six component families are **style-driven**, modeled on SwiftUI's `ButtonStyle`: a `protocol` with a `Configuration` (the composed content + chrome tokens) and a `makeBody(configuration:)` requirement. Implement one and set it with the family's `*Style(_:)` modifier to reskin the container **without forking the component**. | Protocol | Apply with | Covers | Built-in styles | | --- | --- | --- | --- | | `CardStyle` | `.cardStyle(_:)` | cards (Card, RadioCard, CheckboxCard, MenuCard…) | `.default`, `.outlined` | | `FieldStyle` | `.fieldStyle(_:)` | text fields (TextInput, DateField, ColorField, OTPInput…) | `.default`, `.muted`, `.underlined` | | `ChipStyle` | tonal/solid chip styles | Chip | `.tonal`, `.solid` | | `BarStyle` | `.barStyle(_:)` | bottom/booking bars | `.default`, `.floating` | | `MeterStyle` | `.meterStyle(_:)` | progress/meters (ProgressBar, RadialProgress, GaugeView) | `.linear`, `.striped`, `.radial` | | `ToastStyle` | `.toastStyle(_:)` | toasts (AlertToast) | `.default`, `.capsule` | ```swift Card(title: "Booking") { body }.cardStyle(.outlined) TextInput("Email", text: $email).fieldStyle(.underlined) ``` See **llms-patterns.txt** for how to implement a custom `CardStyle`. --- ## Code generation rules Follow these when generating ThemeKit code — they are the difference between correct, token-bound output and code that won't compile or won't re-skin. 1. **Never hardcode a color.** No `.foregroundStyle(.blue)` / `Color(hex:)` / `Color.red` in app UI. Always a token: `theme.text(.textPrimary)`, `theme.background(.bgWhite)`, or a `SemanticColor`. 2. **Read the theme from the environment:** `@Environment(\.theme) private var theme`. 3. **Inject once at the root:** `.environment(Theme.shared)`. 4. **Required content/bindings/actions go in `init`.** Everything else is a modifier — variants, sizes, flags, colors, callbacks. 5. **Chain modifiers; don't add init args.** `Badge("New").badgeStyle(.info).badgeShape(.rounded)`. 6. **Native modifiers:** `.controlSize(_:)` for size, `.disabled(_:)` for disabled — never `size:` / `isEnabled:` init args. Many components also expose `.a11yID(_:)` (check the component's modifier list); it is not a global `View` modifier, so for components without it use SwiftUI's native `.accessibilityIdentifier(_:)`. 7. **No literal radius/spacing:** `Theme.RadiusRole.box.value`, `Theme.SpacingKey.md.value`. 8. **Recolor** with `Theme.shared.applyGenerated(primaryHex:)`, `Theme.shared.apply(ThemeConfig(…))`, or `ThemePreset.named("…")?.apply()`; scope with `.theme(customTheme)`. 9. **Reuse components** — don't re-implement a Card / Sheet / Toast / field. 10. **Look up the exact API** in llms-components.txt (or the MCP `get_component_api`) before inventing modifier names. --- ## Components reference Full per-component API (init + modifiers + usage) is in **llms-components.txt** (119 components documented in depth) and via the MCP `get_component_api` / `get_usage_snippet` tools. The full catalog is **205** components across three tiers: - **Atoms (49)** — smallest primitives: `Avatar`, `Badge`, `Chip`, `Icon`, `Rating`, `ProgressBar`, `Spinner`, `Skeleton`, `Tag`, `Title`, `RollingNumber`, `QRCode`, `Barcode`, … - **Molecules (88)** — inputs/buttons/selectors/small layouts: `TextInput`, `Select`, `Checkbox`, `RadioGroup`, `SegmentedControl`, `Slider`, `RangeSlider`, `OTPInput`, `SearchBar`, `Pagination`, `PrimaryButton`/`SecondaryButton`/`OutlineButton`/`GhostButton`/`ThemeButton`, `Steps`, `Stat`, … - **Organisms (68)** — full sections & surfaces: `Card`, `DataTable`, `Accordion`, `Carousel`, `NavigationBar`, `Sidebar`, `Timeline`, `AlertToast`, `InfoBanner`, `Hero`, `ListView`, `EmptyState`, `ResultView`, `Upload`, plus travel/booking surfaces (`FlightCard`, `SeatMap`, `BoardingPass`, `HotelResultCard`, `PriceBreakdown`, …). The configurable components take a `SemanticColor` + `FillVariant`, so brand/accent recolor for free. A few representative calls: ```swift Badge("Sale").badgeStyle(.error).variant(.solid) Chip("Pool", isSelected: $on).icon("drop.fill") Select("Country", options: countries, selection: $country) { $0.name } Rating(value: 4.5).allowHalf().onRate { rating = $0 } ProgressBar(value: 0.6).showsPercentage().gradient() Card(title: "Booking") { /* content */ }.cardStyle(.outlined) ``` --- ## Theme presets (34) Color sets inspired by daisyUI. Each carries four signature swatches (primary / secondary / accent / base) and a `ThemeConfig` that reproduces it through `ThemeGenerator`. `default`, `neutral`, `light`, `dark`, `cupcake`, `bumblebee`, `emerald`, `corporate`, `synthwave`, `retro`, `cyberpunk`, `valentine`, `halloween`, `garden`, `forest`, `aqua`, `lofi`, `pastel`, `fantasy`, `wireframe`, `black`, `luxury`, `dracula`, `cmyk`, `autumn`, `business`, `acid`, `lemonade`, `night`, `coffee`, `winter`, `dim`, `nord`, `sunset` ```swift ThemePreset.named("dracula")?.apply() // recolor Theme.shared live Theme.shared.apply(ThemePreset.named("nord")!.config) @State private var active: String? = "cupcake" ThemePicker(selection: $active) // tappable grid of all presets ``` --- ## Patterns & recipes Full recipes with code are in **llms-patterns.txt**. Summary: - **Custom theme** — build a `ThemeConfig` / `applyGenerated(primaryHex:)`; persist and re-apply. - **Per-subtree theming** — `.theme(customTheme)` re-skins one branch only. - **Token-bound custom component** — read `@Environment(\.theme)`, resolve tokens, never literals. - **Custom style** — implement a `CardStyle`/`FieldStyle`/… and set it with `.cardStyle(_:)` etc. - **Forms & validation** — field components + `HelperText`/`InputLabel`; error/helper modifiers. - **Card layouts** — `Card` + `CardStack`; elevation via `.elevation(_:)`. - **Navigation** — `NavigationBar`, `Sidebar`, `SegmentedTabBar`. - **Data display** — `DataTable`, `KeyValueTable`, `ListRow`/`ListView`, `Timeline`, `Stat`. - **Accessibility** — `.a11yID(_:)` / `.a11yLabel(_:)` on components that expose them (not global; use SwiftUI's `.accessibilityIdentifier(_:)` otherwise); 44 pt targets & RTL mirroring are built in. - **Localization** — String Catalog (English default), every string overridable via a parameter. --- ## Integration (AI-native) ### MCP server (`@isamercan/themekit-mcp`) — 22 tools Discovery & API: `usage_guide`, `list_components`, `search_components`, `get_component_api`, `get_variants_states`, `get_usage_snippet`, `get_design_tokens`. Theming: `list_themes`, `theme_colors`, `theme_snippet`, `generate_theme`, `design_md_to_themeconfig`. Authoring & verification: `scaffold_screen`, `compose_screen`, `lint_snippet`, `validate_code`, `a11y_audit`, `migrate_snippet`, `get_migration_guide`. Figma round-trip: `design_via_figma_mcp`, `export_figma_variables`, `import_figma_variables`. ### Claude Code agent skill `skills/themekit/SKILL.md` gives an agent the golden rules, setup, token cheatsheet, patterns and anti-patterns; `references/components.md` and `references/themes.md` back it. It triggers on "ThemeKit", design tokens, theme switching, or "build a screen with ThemeKit". ### Figma round-trip - `export_figma_variables` — emit ThemeKit tokens as Figma Variables. - `import_figma_variables` — pull a Figma variable collection back into a `ThemeConfig`. - `design_via_figma_mcp` — ThemeKit's MCP acts as a **client** of Figma's own MCP to design from an existing file/component. ### Design Mode Re-skin from a plain-language `design.md` brief: `design_md_to_themeconfig` parses a design spec into a `ThemeConfig` you apply at runtime — recolor the whole system without editing code. --- ## API reference DocC: **https://isamercan.github.io/ThemeKit/api/documentation/themekit** Docs site: **https://isamercan.github.io/ThemeKit/** · GitHub: **https://github.com/isamercan/ThemeKit** · Wiki: **https://github.com/isamercan/ThemeKit/wiki**