---
title: Theming
description: CSS variable token system, theme presets (Default, Minimal, Soft, Material, Brutalist, Retro Terminal, Cupertino) and the Liquid Glass extension
visibility: guest
draft: false
---

# Theming

WireKit uses a comprehensive design token system built on CSS variables. Every visual property — colors, radius, shadows, typography, motion, sizing — can be customized by overriding tokens in your `app.css`.

## How It Works

WireKit ships with a default theme defined in `wirekit.css`, loaded separately via the `@wirekitStyles` Blade directive. All components reference these tokens exclusively — no hardcoded colors, sizes, or durations.

Override any token in your `app.css` — the overrides cascade on top of the separately loaded `wirekit.css`. Use a plain `:root {}` block; the `--color-wk-*` / `--radius-wk-*` / `--shadow-wk-*` tokens are CSS custom properties, NOT Tailwind theme tokens, so they don't need an `@theme {}` wrapper:

```css
@import 'tailwindcss';

/* Override WireKit tokens */
:root {
    --color-wk-accent: var(--color-blue-600);
    --radius-wk: 1rem;
    --shadow-wk-sm: none;
}
.dark {
    --color-wk-accent: var(--color-blue-400);
}
```

> **`:root {}` vs `@theme {}` vs `@layer base {}` — what each one does:**
>
> All three set CSS custom properties; pick the one that matches your intent.
>
> - **`:root {}` — recommended for WireKit token overrides.** Plain CSS, no Tailwind compiler involvement. The `--color-wk-*` / `--radius-wk-*` / `--shadow-wk-*` tokens are custom properties consumed directly by `var()` calls inside Blade templates, so a `:root` override has the exact same effect at runtime as `@theme` and is the simplest shape to read.
> - **`@theme {}`** — Tailwind v4-specific. Tells the compiler to generate utility classes for the tokens declared inside (e.g. `--color-brand: #ab35ff` inside `@theme` makes `bg-brand` / `text-brand` available as utilities). For overriding EXISTING WireKit tokens, this is functionally identical to `:root` because the tokens already produce no extra utilities. The preset blocks below use `@theme` for historical reasons; you can safely rewrite them as `:root` and the result is the same.
> - **`@layer base {}`** — Tailwind v3-era pattern, still works in v4. Wraps base-layer rules so they cascade correctly. **Don't wrap WireKit token overrides in it:** `wirekit.css` ships its defaults as *unlayered* CSS, and unlayered CSS always wins over layered CSS regardless of specificity — so a `--color-wk-*` override inside `@layer base` (or any `@layer`) silently loses to the WireKit default and has no visible effect. Use it only when you have actual base-layer style declarations to scope, never for token overrides.
>
> **Bottom line:** for the "I want a different accent color" use case, `:root {}` is the cleanest pattern. The `@theme {}` blocks below produce identical output.

::: warning
Do NOT `@import` `wirekit.css` in your `app.css` — it contains Tailwind v4 directives that conflict with the Tailwind Vite plugin. Use `@wirekitStyles` in your layout instead.
:::

Dark mode works automatically. The `.dark` class on `<html>` or `<body>` switches all tokens to their dark variants via `@layer theme`.

## Font Presets

WireKit ships locally bundled, GDPR-compliant Google Fonts that activate via the `fonts` config and `<x-wirekit::fonts />` component. The dedicated [Fonts component page](/components/fonts) is the canonical reference — full setup walk-through, available font list per category, behavior under various states.

To expose WireKit fonts to the global Tailwind v4 token surface so non-WireKit elements pick them up too, add this to your `app.css` after configuring fonts per the Fonts page:

```css
@theme {
    --font-sans: var(--font-wk-sans);
    --font-serif: var(--font-wk-serif);
    --font-mono: var(--font-wk-mono);
}
```

## Typography Tokens

WireKit exposes eight body-text sizes plus separate heading-weight controls via CSS variables. The full table lives in [Design Tokens → Component Sizing](/theming/design-tokens#component-sizing); the summary below covers what each token controls and where it appears.

| Token | Default | Typical use |
| --- | --- | --- |
| `--text-wk-2xs` | 0.6875rem | Micro text — dense secondary labels (e.g. an event time under its title) |
| `--text-wk-xs` | 0.75rem | Caption / micro-label / footer text |
| `--text-wk-sm` | 0.8125rem | Small body / secondary text |
| `--text-wk-md` | 0.875rem | Default body font size |
| `--text-wk-lg` | 1rem | Large body / lede paragraph |
| `--text-wk-xl` | 1.25rem | Sub-heading |
| `--text-wk-2xl` | 1.5rem | Section heading |
| `--text-wk-3xl` | 1.875rem | Page heading |
| `--measure-wk` | 65ch | Readable line-length clamp for long-form `<x-wirekit::prose>` (default `measure`) |
| `--measure-wk-wide` | 78ch | Roomier line-length clamp (`prose measure="wide"`) |

::: info
**Why preset blocks below only override `sm/md/lg`:** the eight sizes form a fluid scale anchored at the body size (`--text-wk-md`). Each preset adjusts the body-scale band (small / default / lead) so paragraphs feel right for that visual style; the heading sizes (`xl`/`2xl`/`3xl`) and the micro / caption sizes (`2xs`/`xs`) keep proportional defaults derived from the body band. If you want an end-to-end custom scale (e.g. for a brand with bigger headings), override every `--text-wk-*` token in your `:root` block — see Design Tokens for the worked example.
:::

## Accent Color

The accent color controls primary buttons, focus rings, and interactive elements:

```css
@theme {
    --color-wk-accent: var(--color-blue-600);
    --color-wk-accent-hover: var(--color-blue-700);
    --color-wk-accent-content: var(--color-blue-700);
    --color-wk-accent-fg: var(--color-white);
    --color-wk-ring: var(--color-blue-600);
}
@layer theme {
    .dark {
        --color-wk-accent: var(--color-blue-500);
        --color-wk-accent-hover: var(--color-blue-400);
        --color-wk-accent-content: var(--color-blue-400);
        --color-wk-ring: var(--color-blue-500);
    }
}
```

## Dark mode

Every token has a dark counterpart under `.dark`, so switching the theme is one
class on `<html>` — there is no second set of components to maintain.

You do not have to wire that yourself. [Theme controller](/components/theme-controller)
ships the control, the persistence, and the head script that stops the page
flashing white before it turns dark:

```blade
<head>
    @wirekitThemeScript   {{-- 1. Applies the stored theme before the first paint --}}
    @wirekitStyles
</head>

<body>
    <x-wirekit::theme-controller />   {{-- 2. The control itself --}}
</body>
```

With nothing stored, the page follows the reader's operating system — and keeps
following it, live, if their machine turns dark at sunset. An explicit choice wins
and is remembered.

Anything with its own colors can listen for the change instead of polling:

```blade
<div x-data x-on:wirekit:theme-changed.window="applyTheme($event.detail.dark)">…</div>
```

See [theme controller](/components/theme-controller) for the variants and the
storage key.

### What the class carries besides tokens

Alongside the tokens, both blocks declare
[`color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme).
That is the only signal the browser reads for the widgets **it** paints rather
than we do: the popup a native `<select>` opens, the document scrollbar, number
spinners, the date, time and color pickers, autofill highlighting, the spellcheck
underline. Without it those stay light on a dark page — a white dropdown over a
near-black surface — however the tokens are set.

It also decides where the class belongs. `color-scheme` governs the controls
inside the element carrying it, but the document canvas and the document
scrollbar follow the root element only. Put `.dark` on `<html>` — where
`@wirekitThemeScript` puts it — rather than on a wrapper `<div>`, or you get dark
controls inside a light-scrollbarred page.

To decide it yourself, declare it in your own `:root`, which outranks WireKit's
default regardless of load order:

```css
:root { color-scheme: light dark; }   /* hand the native controls straight to the OS */
```

## Theme Presets

WireKit ships eight theme presets. Each one now lives on its own page with the full copy-paste block, prerequisites, and WCAG notes — so this guide stays focused on the shared theming concepts. Apply any preset with `php artisan wirekit:theme <name>`, or copy its CSS block into your `app.css`.

- **[Theme: Default](/theming/default)** — the refined neutral baseline (ships out of the box, no CSS needed).
- **[Theme: Aurora](/theming/aurora)** — modern, color-confident, toned to the brand magenta, with one-line `--theme-hue` retinting and ready-made color presets.
- **[Theme: Brutalist](/theming/brutalist)** — high-contrast, hard edges, thick borders, zero radius.
- **[Theme: Cupertino](/theming/cupertino)** — Apple-inspired translucency (includes the Liquid Glass extension).
- **[Theme: Material](/theming/material)** — Material Design 3, elevated surfaces, indigo accent, Roboto.
- **[Theme: Minimal](/theming/minimal)** — radical reduction for data-dense interfaces and admin tools.
- **[Theme: Retro Terminal](/theming/retro-terminal)** — green-on-black terminal aesthetic, monospace stack.
- **[Theme: Soft](/theming/soft)** — warm, rounded developer-SaaS aesthetic (violet accent, DM Sans).

> For the complete CSS-variable reference, see **[Design Tokens](/theming/design-tokens)**.

## Responsive token overrides

The bundled section-spacing tokens (`--space-wk-section-sm` = 3 rem, `--space-wk-section-md` = 5 rem, `--space-wk-section-lg` = 7 rem) are read directly by `<x-wirekit::hero size="…">` and `<x-wirekit::cta size="…">`. The component itself ships a responsive utility: the named size applies at the `sm+` breakpoint, and the mobile viewport (< sm) drops one tier automatically. For most projects that is the right shape — no developer-side work needed.

The **advanced pattern** for projects that want to flip the underlying token values themselves per viewport — not just the tier-binding on the component — lives in your `app.css`:

```css
:root {
    --space-wk-section-sm: 2rem;
    --space-wk-section-md: 4rem;
    --space-wk-section-lg: 6rem;
}

@media (min-width: 768px) {
    :root {
        --space-wk-section-sm: 3rem;
        --space-wk-section-md: 5rem;
        --space-wk-section-lg: 7rem;
    }
}

@media (min-width: 1280px) {
    :root {
        --space-wk-section-sm: 4rem;
        --space-wk-section-md: 7rem;
        --space-wk-section-lg: 10rem;
    }
}
```

Why use this over the component's `size` prop alone? Three cases:

1. **Custom breakpoints.** The component's built-in responsive utility flips at Tailwind's `sm` breakpoint (640 px). If your design system uses different breakpoints (e.g. 768 / 1024 / 1280), the media-query override pattern lets every section-spacing developer track the same custom break-points without per-component overrides.
2. **Three-tier-on-three-viewport semantics.** The component's utility shifts ONE tier between mobile and `sm+`. If you want a tighter mobile + comfortable tablet + roomy desktop progression (three values across three breakpoints), the override pattern delivers it.
3. **Cross-primitive coherence.** Any other developer of `--space-wk-section-*` — including any custom primitives you author — automatically tracks the same scale. The component-level `size` prop only controls hero/cta.

::: tip
Combine the two approaches: keep the component-level `size` prop for per-section intent ("this hero feels `lg`, this cta feels `md`"), and use the media-query override to globally tune what `sm` / `md` / `lg` mean across your design system.
:::

::: tip
Thanks to the `:where()` spec-0 wrap on WireKit's token blocks, a plain `:root {}` override wins regardless of load order — so place these declarations in the same `:root {}` block where you set `--color-wk-accent` and your other customizations, wherever your build emits it.
:::

The same pattern applies to any other `--space-wk-*` token (`--space-wk-md`, `--space-wk-xl`, etc.) — every token in WireKit's design system is developer-overridable via a `:root {}` declaration. The section-spacing scale is the one developers most often want to flip per viewport, which is why it gets its own documented recipe.

### Toast edge offset

`<x-wirekit::toast-region>` pins its stack flush to a viewport edge (`top-*` or `bottom-*`). If your layout has a fixed header / navigation bar, set `--space-wk-toast-offset` to its height so toasts never render underneath it:

```css
@layer base {
    :root {
        /* 1. Keep top-positioned toasts clear of a 4rem fixed header. */
        --space-wk-toast-offset: 4rem;
    }
}
```

| Token | Default | Used for |
|---|---|---|
| `--space-wk-toast-offset` | `0px` | Distance the toast stack keeps from its pinned edge — set to your fixed header / nav (or bottom-bar) height |

The component folds `env(safe-area-inset-top)` (for `top-*` positions) or `env(safe-area-inset-bottom)` (for `bottom-*` positions) in on top of this value automatically, so the notch and home indicator are always cleared regardless of what you set. The offset applies only to the active edge; the other three sides keep their default padding.

## Motion tokens

::: info Reduced motion is honored across the library
A visitor who has asked their system to reduce motion sees WireKit's transitions
and animations resolve immediately — every component, not a selected few. You do
not need to opt in, and there is no token to set.

The rule is scoped to WireKit's own elements, so your animations are left alone:
if you want them to respect the same preference, write your own
`@media (prefers-reduced-motion: reduce)` block.
:::

The motion-rhythm token surface drives every animated component (`<x-wirekit::reveal>`, `<x-wirekit::feature-grid stagger>`, `<x-wirekit::stats stagger>`, every `animateIn` developer) plus the developer-side `wk-animate-*` utility classes:

| Token | Default | Used for |
|---|---|---|
| `--motion-wk-duration-fast` | `150ms` | `<x-wirekit::reveal duration="fast">` |
| `--motion-wk-duration-normal` | `300ms` | `<x-wirekit::reveal duration="normal">` (default) |
| `--motion-wk-duration-slow` | `600ms` | `<x-wirekit::reveal duration="slow">` |
| `--motion-wk-delay-{none,sm,md,lg,xl}` | `0ms / 75ms / 150ms / 300ms / 500ms` | `<x-wirekit::reveal delay="…">` |
| `--motion-wk-easing-out` | `cubic-bezier(0.16, 1, 0.3, 1)` | Decelerate-style entrances |
| `--motion-wk-easing-in` | `cubic-bezier(0.7, 0, 0.84, 0)` | Accelerate-style exits |
| `--motion-wk-easing-spring` | `cubic-bezier(0.5, 1.5, 0.5, 1)` | Bounce / spring presets |
| `--wk-stagger-step` | `75ms` | Per-child increment driving the `.wk-stagger > *:nth-child(N)` cascade. Emitted by `<x-wirekit::feature-grid stagger>` and `<x-wirekit::stats stagger>`. Override per-instance via inline `style="--wk-stagger-step: 200ms"` OR globally via `:root {}` for a design-system-wide cascade rhythm. Honors `prefers-reduced-motion: reduce` — every child's delay collapses to 0. |
| `--shimmer-wk-duration` | `2s` | Travel duration of the text-glyph shimmer (`<x-wirekit::shimmer>` / `.wk-text-shimmer`). Override per-instance via the component's `duration` prop or inline `style="--shimmer-wk-duration: 3s"`. Disabled under `prefers-reduced-motion: reduce`, `forced-colors: active`, and `prefers-reduced-transparency: reduce`. |
| `--shimmer-wk-band` | `3ch` | Half-width of the bright traveling highlight band in the text-glyph shimmer. Smaller values give a tighter, sharper glint; larger values a broader wash. Override per-instance via inline `style="--shimmer-wk-band: 1.5ch"`. |
| `--shimmer-wk-angle` | `105deg` | Tilt of the shimmer's highlight band. Override per-instance via inline `style="--shimmer-wk-angle: 90deg"` for a vertical sweep. |

See [Public CSS API → wk-stagger](extending/public-css-api.md#animation--motion) for the full contract on the stagger class.

## Theme markers

Some themes go past tokens and dress specific surfaces — adding a glass class to every panel, say. That needs a stable way to find those surfaces, so the components below emit a `data-wk-*` attribute for exactly that purpose. **These are the complete set**; anything not listed here has no marker, and a selector aiming at one will match nothing.

| Surface | Marker | Emitted by |
| --- | --- | --- |
| Card | `data-wk-card` | `<x-wirekit::card>` |
| Dropdown panel | `data-wk-dropdown-panel` | `<x-wirekit::dropdown.panel>` |
| Popover panel | `data-wk-popover` | `<x-wirekit::popover>` |
| Modal header | `data-wk-modal-header` | `<x-wirekit::modal.header>` |
| Modal body | `data-wk-modal-body` | `<x-wirekit::modal.body>` |
| Drawer body | `data-wk-drawer-body` | `<x-wirekit::drawer.body>` |

### One marker that is not a surface

`data-wk-tip` is in the package and looks like it belongs in the table above. **It does not, and mapping a theme to it does the wrong thing while appearing to work.**

It marks an element that **has** a tooltip — a truncated event title, a chip, a row — not a tooltip itself. A theme that reads the name as "tooltip" and dresses it frosts the calendar's event pills and never touches a single tooltip. That failure is worse than reaching nothing, because something visibly changes and the map looks correct.

The tooltip itself carries no themeable marker at all, for the reason in the next section: it does not exist until it opens.

::: warning
**Never assume a theme selector matches anything — check it.** A selector that matches nothing is the quietest failure available: it throws no error, logs no warning, and produces no visual difference you could compare against, so the theme silently does less than its source file says. You cannot see this by reading the map; assert that each mapping matches at least one element, or the gap sits unnoticed for as long as the map exists.
:::

### Runtime-created surfaces are outside a theme's reach

A tooltip is the clear case. Its element does not exist in the server-rendered document — `wirekit.js` creates it and stamps `data-wk-tip` at the moment the tip opens. A theme that sweeps the document on load, or on `livewire:navigated`, looks before the node exists and finds nothing. **No selector fixes this**, because the problem is timing rather than naming.

If you need to dress a surface like that, observe insertions (a `MutationObserver` on `document.body`, filtered to the marker) rather than sweeping once. The same applies to any element a component creates on interaction rather than on render.

## Accessibility & Contrast

Every default token pair in WireKit is verified against [WCAG 2.2](https://www.w3.org/TR/WCAG22/) contrast requirements. The audit covers every bundled theme preset — **Default**, **Minimal**, **Soft**, **Material**, **Brutalist**, **Retro Terminal**, **Cupertino**, **Aurora** — in **both light and dark mode**.

### Guaranteed compliance

The following pairs meet **WCAG AA normal-text** (≥ 4.5:1) across every preset × mode combination:

- **Body text** (`text` on `bg`, `bg-elevated`, `bg-subtle`)
- **Hint text** (`text-muted` on `bg`, `bg-elevated`, `bg-subtle`, and on the tinted
  surfaces of `callout` / `alert`)
- **Helper/placeholder text** (`text-subtle`, `text-placeholder` on `bg`, `bg-input`)
  — these two are guaranteed on those surfaces only. A `subtle` line placed on a
  tinted `callout` / `alert` surface is not covered; use `muted` there, which is.
- **Primary buttons** (`accent-fg` on `accent`, `accent-hover`)
- **Semantic buttons** (`danger-fg` on `danger`/`danger-hover`, `success-fg` on `success`, `warning-fg` on `warning`)
- **Semantic message text** (`danger-text`, `success-text` on `bg`/`bg-elevated`)
- **Focus rings** (`ring` on `ring-offset`, ≥ 3:1 non-text per WCAG 1.4.11)

::: tip
Success and warning buttons use **dark foreground** (`neutral-900`) on vivid bg colors, not white. White-on-amber-500 only reaches 2.15:1 — far below AA. This is the single most common contrast bug in off-the-shelf design systems, and WireKit refuses to ship it.
:::

### Intentional trade-offs

Two categories of token pairs intentionally sit below the strict 4.5:1 / 3:1 thresholds:

1. **Decorative borders** (`border`, `border-subtle`, `border-hover`) — these sit at ~1.3-2.5:1 against the page background. [WCAG 1.4.11](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html) requires 3:1 only for UI elements that **convey state or identify boundaries**. Pure decorative dividers are exempt. Every major design system (Shadcn UI, Radix, Tailwind UI, Material, Chakra) uses identical values. Stateful borders that *do* communicate (focus rings, `border-error`, `border-success`) meet or exceed 3:1. The built-in `wirekit:doctor:a11y --theme-contrast` audit reflects exactly this split: it reports the resting decorative borders as advisory `INFO (decorative, WCAG 1.4.11 exempt)` and hard-checks the focus ring plus the stateful borders at 3:1 — so a default install audits clean.

> **`border-strong` and `border-strong-hover` are NOT in that list.** They are the resting and hover border of every form control, so they carry the boundary of an interactive element and are contrast-bound: ≥3:1 against the input fill, in both themes. `border-strong-hover` additionally has to move *away* from the fill — a hover that lightens toward the field, as `border-hover` does, drops the control below the threshold exactly while it is being used. A preset that retints `border` for its controls must set these two as well, or its inputs fall back to the stock neutral.

2. **`text-muted` on `bg-muted`** (~4.1-4.4:1) — this pair narrowly misses AA normal-text but exceeds AA Large Text (3:1) comfortably. `bg-muted` is used for chips, pills, and secondary buttons where text is typically 14px semibold or larger (qualifying as "large text" per WCAG). For body copy *inside* a muted surface, use `text` (primary), not `text-muted`.

### Placeholder text

Per [WCAG F24](https://www.w3.org/WAI/WCAG22/Techniques/failures/F24) placeholders count as "content that can be dismissed by user action", but WireKit still ships `text-placeholder` at the same 4.5:1 minimum as body text (neutral-500 light / neutral-400 dark) because the bar should be "always legible," not "legally compliant." We match `text-subtle` for consistency.

### Running your own audit

If you customize tokens (new accent palette, dark mode inversion, etc.) and want to verify AA compliance, start with WireKit's built-in audit, then reach for an external tool for anything it doesn't cover:

- **`php artisan wirekit:doctor:a11y --theme-contrast`** — the built-in audit. Reads your `app.css` token overrides (filling any gap from the shipped defaults) and reports WCAG 2.1 contrast per canonical pairing × light/dark mode. Per WCAG 1.4.11 the *communicating* borders (focus `ring`, `border-error`, `border-success`) are checked at 3:1 while the *resting decorative* borders are flagged advisory-exempt, so a default install audits clean.
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) — one pair at a time
- [Pa11y](https://pa11y.org/) — automated page scanning
- Chrome DevTools → Inspect → Accessibility pane → Contrast ratio

Minimum targets:

- **Normal text**: 4.5:1 (AA), 7:1 (AAA)
- **Large text** (≥ 18pt or ≥ 14pt bold): 3:1 (AA), 4.5:1 (AAA)
- **UI components, meaningful borders, focus indicators**: 3:1

### Syntax-highlighter contract

WireKit ships the design tokens but does **not** ship a syntax highlighter — `highlight.js`, Prism, Shiki, or any other choice remains your responsibility. WireKit's own `<x-wirekit::code-block>` component renders the surface (`<pre>` background, padding, rounded corners) using `--color-wk-bg-elevated` and `--color-wk-text` for the body text, but token-level coloration is delegated to whichever highlighter you wire up.

If you load a syntax-highlighter theme on top of `<x-wirekit::code-block>`, **the theme's per-token colors must meet WCAG 2.1 AA (≥4.5:1) against the active `--color-wk-bg-elevated`, in BOTH light and dark mode, across every theme preset you ship to your users.** This is non-negotiable — code blocks are body text, and 4.5:1 is the threshold.

Two foot-guns to watch for:

1. **Light-mode WCAG overrides that bleed into dark mode.** A common pattern is to load a base highlight.js theme (`github.min.css`) and then override individual `.hljs-*` token colors in your own CSS to hit AA against your light surface. If those overrides are unscoped (e.g. plain `.docs-prose pre .hljs-keyword { color: ... }`), they'll keep winning the cascade against your dark-mode bundle (`github-dark-dimmed.min.css`) too — painting dark light-mode colors onto a dark background. **Always scope WCAG overrides with `html:not(.dark)`** (or your equivalent dark-mode root selector) so they drop out of the cascade in dark mode.

2. **Per-theme `--color-wk-bg-elevated` variations.** Different theme presets ship different elevated-surface tones (Retro Terminal `#1a1a1a`, Cupertino `#2C2C2E`, Default `oklch(20.5% 0 0)` ≈ `#353535`, etc.). A token palette that passes 4.5:1 against one preset's bg-elevated may fail against another. Audit token contrast on every preset × every mode combination, not just one pair.

A practical audit recipe (Playwright + WCAG 2.1 contrast computation):

```js
// pseudo-code: iterate themes × modes × representative pages,
// walk every `.docs-prose pre code.hljs span[class^="hljs-"]`,
// compute getComputedStyle().color vs effective non-transparent
// background, fail the build on any sample below 4.5:1.
for (const theme of THEMES) {
  for (const mode of ['light', 'dark']) {
    await page.goto(`...`);
    await page.evaluate(([t, m]) => {
      localStorage.setItem('wirekit-theme', t);
      localStorage.setItem('wirekit-dark-mode', m === 'dark' ? '1' : '0');
    }, [theme, mode]);
    await page.reload();
    // ... walk DOM, compute contrast, exit non-zero on failure
  }
}
```

If you ship syntax-highlighted code blocks in your own developer app, wire an equivalent contrast audit into your CI — walk every theme × mode × representative page, compute foreground-vs-background contrast for every `.hljs-*` token-class, and fail the build below the WCAG 4.5:1 threshold for normal text.

## Design Token Reference

The complete CSS-variable surface — every `--color-wk-*`, `--radius-wk-*`, `--shadow-wk-*`, motion / typography / sizing token, plus the reading-* component family and chart-theming palette — lives on its own dedicated page so this guide can stay focused on setup and theming concepts.

→ **[Design Tokens](/theming/design-tokens)** — full reference

---

The token surface is intentionally orthogonal to the theme presets above: change a preset to swap visual style, change a token to fine-tune a specific dimension across every component that reads from it.
