# ThemeKit — Patterns & Recipes > Copy-paste recipes for common ThemeKit tasks. Every snippet is token-bound: it reads the > theme from the environment and never hardcodes a color, radius, or spacing. Companion files: > **llms.txt** (index) · **llms-full.txt** (architecture + tokens) · **llms-components.txt** > (per-component API). **The one rule that makes all of these work:** `@Environment(\.theme) private var theme`, inject `.environment(Theme.shared)` once at the root, and pull tokens — `theme.text(.textPrimary)`, `theme.background(.bgWhite)`, `Theme.SpacingKey.md.value`, `Theme.RadiusRole.box.value`. --- ## Pattern: Custom theme creation Generate a whole palette from one accent, or specify a full config. ```swift // From a single accent — ThemeGenerator expands the full Ant-style palette on-device: Theme.shared.applyGenerated(primaryHex: "7C3AED") // Full recipe: accent + base surface + brand secondary/accent, light or dark, tint strength: let config = ThemeConfig(primaryHex: "FF79C6", // brand accent baseHex: "282A36", // surface "paper" tone secondaryHex: "BD93F9", accentHex: "FFB86C", tint: 0.07, // how far the accent bleeds into neutrals dark: true) Theme.shared.apply(config) ``` Persist the user's choice and re-apply on launch: ```swift // Save UserDefaults.standard.set("dracula", forKey: "themeID") // Restore at startup (before the first view renders) if let id = UserDefaults.standard.string(forKey: "themeID") { ThemePreset.named(id)?.apply() } ``` --- ## Pattern: Per-subtree theming `.theme(_:)` injects a theme into ONE branch of the view tree — the rest of the app keeps `Theme.shared`. Because components read the *environment* theme, everything below re-skins. ```swift struct DualThemeScreen: View { var body: some View { VStack(spacing: Theme.SpacingKey.lg.value) { BookingCard() // uses the app theme (Theme.shared) let ocean = Theme() ocean.applyGenerated(primaryHex: "0FB4AB") PromoBanner("Summer sale") .theme(ocean) // this branch only is teal } } } ``` Use it for preview galleries, A/B skins, or a "try this theme" card that recolors just itself. --- ## Pattern: Token-bound custom component When you must write your own view, read the theme and resolve tokens — never literals. ```swift struct PriceTagView: View { @Environment(\.theme) private var theme let amount: String var body: some View { Text(amount) .textStyle(.labelMd700) // typography token .foregroundStyle(theme.foreground(.fgHero)) // color token .padding(.horizontal, Theme.SpacingKey.sm.value) // spacing token .padding(.vertical, Theme.SpacingKey.xs.value) .background(theme.background(.bgSecondary)) // surface token .clipShape(RoundedRectangle(cornerRadius: Theme.RadiusRole.selector.value, style: .continuous)) // radius role token } } ``` Rules: pull every color from `theme.*` (or a `SemanticColor`), size text with `.textStyle(_:)`, space with `Theme.SpacingKey`, round with `Theme.RadiusRole` / `Theme.RadiusKey`. --- ## Pattern: Token-bound custom style (flexibility architecture) The six style protocols (`CardStyle`, `FieldStyle`, `ChipStyle`, `BarStyle`, `MeterStyle`, `ToastStyle`) let you reskin a component family without forking it — the SwiftUI `ButtonStyle` shape. Implement `makeBody(configuration:)` and read tokens from the configuration + environment. ```swift // A glass card container. The configuration carries the composed content plus chrome tokens // (surfaceKey, radius role, elevation, selection/press state). struct GlassCardStyle: CardStyle { func makeBody(configuration: CardStyleConfiguration) -> some View { GlassCardSurface(configuration: configuration) } } private struct GlassCardSurface: View { let configuration: CardStyleConfiguration @Environment(\.theme) private var theme private var shape: RoundedRectangle { RoundedRectangle(cornerRadius: configuration.radius.value, style: .continuous) } var body: some View { configuration.content .background(theme.background(configuration.surfaceKey).opacity(0.65)) .clipShape(shape) .overlay( shape.stroke( theme.border(configuration.isSelected ? .borderHero : .borderPrimary), lineWidth: configuration.isSelected ? 1.5 : 1 ) ) } } // Apply it (or one of the built-ins: .default, .outlined): Card(title: "Booking") { bookingBody }.cardStyle(GlassCardStyle()) Card(title: "Summary") { summaryBody }.cardStyle(.outlined) ``` Field styles work the same way — `TextInput("Email", text: $email).fieldStyle(.underlined)` or a custom `some FieldStyle`. --- ## Pattern: Form building Compose field components inside a `Card`; label with `InputLabel`, hint with `HelperText`, and drive state with bindings. Sizes are `.controlSize(_:)`, disabled is `.disabled(_:)`. ```swift struct SignUpForm: View { @Environment(\.theme) private var theme @State private var email = "" @State private var password = "" @State private var agree = false @State private var emailError: String? var body: some View { Card(title: "Create account") { VStack(spacing: Theme.SpacingKey.md.value) { TextInput("Email", text: $email) .icon(leading: "envelope") .errorText(emailError ?? "") // non-empty → field shows the error state .a11yID("signup.email") TextInput("Password", text: $password) .secure(true) // secure entry is a modifier, not an init arg .a11yID("signup.password") Checkbox("I accept the terms", isChecked: $agree) // init: (_ label, isChecked:) .controlSize(.small) PrimaryButton("Create account") { submit() } .fullWidth() .disabled(!agree || email.isEmpty) } } } private func submit() { emailError = email.contains("@") ? nil : "Enter a valid email" } } ``` Validation: drive `.errorText(_:)` (or `.helperText(_:)` / `.warningText(_:)`) from your own validation state — a non-empty error string flips the field to the error token automatically. For richer inline messaging use `.infoMessages(_:)`. --- ## Pattern: Card-based layout `Card` is the container; `CardStack` maps a collection to a card per item. Elevation is a token via `.elevation(_:)` (`CardElevation`: `.none` / `.soft` / `.elevated`); `.cardStyle(_:)` restyles the shell. ```swift // CardStack(_ items:) { item in … } — one card per element, no manual ForEach. CardStack(destinations) { d in Card(title: d.name) { RemoteImage(d.imageURL) // init(_ url: URL?) — positional Text(d.subtitle).textStyle(.bodySm400) } .subtitle(d.country) .elevation(.soft) .cardStyle(.outlined) } ``` --- ## Pattern: Navigation ```swift // Top header with a back action — PageHeader(_ title:) PageHeader("Explore") .subtitle("48 destinations") .onBack { dismiss() } // Sidebar (iPad / macOS) — Sidebar(items:selection:) with a String? selection binding Sidebar(items: sidebarItems, selection: $section) // sidebarItems: [Sidebar.Item] .width(260) // Tab-style navigation across an Int index — SegmentedTabBar(_ items:selection:) SegmentedTabBar(["Overview", "Reviews", "Map"], selection: $tab) .tabStyle(.underline) // SegmentedTabBarStyle: .underline / .card ``` --- ## Pattern: Data display ```swift // Tabular data — DataTable(columns:rows:). Build the columns once as [DataTable.Column]. DataTable(columns: columns, rows: bookings) .striped() .onRowTap { open($0) } // Key/value summary — KeyValueTable(rows:) with [KeyValueTable.Row] KeyValueTable(rows: fareRows) .title("Fare breakdown") .bordered(true) // List — ListView(_ items:) { item in row } ListView(recentSearches) { item in ListRow(item.title) { open(item) } // ListRow(_ title, action:) .subtitle(item.subtitle) .icon(item.icon) } .header("Recent") // Metric + trend — StatTrend is .up(_:) / .down(_:) Stat(title: "Bookings", value: "1,284").trend(.up("+12%")) // Chronology — TimelineMode is .left / .right / .alternate Timeline(bookingEvents).mode(.alternate) ``` > Exact init/modifier signatures vary per component — confirm against **llms-components.txt** or > the MCP `get_component_api` tool before shipping. --- ## Anti-patterns (do NOT do these) - ❌ Hardcoded colors — `.foregroundStyle(.blue)`, `Color(hex: "FF0000")`, `Color.red`. ✅ A token — `theme.text(.textPrimary)`, `theme.background(.bgWhite)`, a `SemanticColor`. - ❌ Reading the theme outside the environment — `Theme.shared.text(...)` inside a subview. ✅ `@Environment(\.theme) private var theme` (so per-subtree `.theme(_:)` is respected). - ❌ Hardcoded spacing/radius — `.padding(16)`, `cornerRadius: 12`. ✅ `Theme.SpacingKey.md.value`, `Theme.RadiusRole.box.value`. - ❌ Size/enabled as init args — `Badge("x", size: .small)`, `Control(isEnabled: false)`. ✅ Native modifiers — `Badge("x").controlSize(.small)`, `Control().disabled(true)`. - ❌ Re-implementing an existing component (a bespoke card/sheet/toast/field). ✅ Use the shipped component and restyle it via its `*Style(_:)` protocol. - ❌ Ignoring the style protocols and overriding a component's internals. ✅ Implement `CardStyle` / `FieldStyle` / … and set it with `.cardStyle(_:)` / `.fieldStyle(_:)`. - ❌ Omitting accessibility — no accessibility id, tap targets under 44 pt. ✅ `.a11yID("screen.element")` on components that expose it (check the modifier list), else SwiftUI's `.accessibilityIdentifier(_:)`; ThemeKit's 44 pt targets & RTL mirroring do the rest. - ❌ Inventing modifier names from memory. ✅ Look them up in **llms-components.txt** or via the MCP `get_component_api`.