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:
@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 byvar()calls inside Blade templates, so a:rootoverride has the exact same effect at runtime as@themeand 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: #ab35ffinside@thememakesbg-brand/text-brandavailable as utilities). For overriding EXISTING WireKit tokens, this is functionally identical to:rootbecause the tokens already produce no extra utilities. The preset blocks below use@themefor historical reasons; you can safely rewrite them as:rootand 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.cssships 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.
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 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:
@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; 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") |
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:
@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 ships the control, the persistence, and the head script that stops the page flashing white before it turns dark:
<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:
<div x-data x-on:wirekit:theme-changed.window="applyTheme($event.detail.dark)">…</div>
See theme controller for the variants and the storage key.
What the class carries besides tokens
Alongside the tokens, both blocks declare
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:
: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 — the refined neutral baseline (ships out of the box, no CSS needed).
- Theme: Aurora — modern, color-confident, toned to the brand magenta, with one-line
--theme-hueretinting and ready-made color presets. - Theme: Brutalist — high-contrast, hard edges, thick borders, zero radius.
- Theme: Cupertino — Apple-inspired translucency (includes the Liquid Glass extension).
- Theme: Material — Material Design 3, elevated surfaces, indigo accent, Roboto.
- Theme: Minimal — radical reduction for data-dense interfaces and admin tools.
- Theme: Retro Terminal — green-on-black terminal aesthetic, monospace stack.
- Theme: Soft — warm, rounded developer-SaaS aesthetic (violet accent, DM Sans).
For the complete CSS-variable reference, see 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:
: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:
- Custom breakpoints. The component's built-in responsive utility flips at Tailwind's
smbreakpoint (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. - 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. - Cross-primitive coherence. Any other developer of
--space-wk-section-*— including any custom primitives you author — automatically tracks the same scale. The component-levelsizeprop only controls hero/cta.
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.
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:
@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
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 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.
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 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 (
textonbg,bg-elevated,bg-subtle) - Hint text (
text-mutedonbg,bg-elevated,bg-subtle, and on the tinted surfaces ofcallout/alert) - Helper/placeholder text (
text-subtle,text-placeholderonbg,bg-input) — these two are guaranteed on those surfaces only. Asubtleline placed on a tintedcallout/alertsurface is not covered; usemutedthere, which is. - Primary buttons (
accent-fgonaccent,accent-hover) - Semantic buttons (
danger-fgondanger/danger-hover,success-fgonsuccess,warning-fgonwarning) - Semantic message text (
danger-text,success-textonbg/bg-elevated) - Focus rings (
ringonring-offset, ≥ 3:1 non-text per WCAG 1.4.11)
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:
- Decorative borders (
border,border-subtle,border-hover) — these sit at ~1.3-2.5:1 against the page background. WCAG 1.4.11 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-inwirekit:doctor:a11y --theme-contrastaudit reflects exactly this split: it reports the resting decorative borders as advisoryINFO (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-strongandborder-strong-hoverare 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-hoveradditionally has to move away from the fill — a hover that lightens toward the field, asborder-hoverdoes, drops the control below the threshold exactly while it is being used. A preset that retintsborderfor its controls must set these two as well, or its inputs fall back to the stock neutral.
text-mutedonbg-muted(~4.1-4.4:1) — this pair narrowly misses AA normal-text but exceeds AA Large Text (3:1) comfortably.bg-mutedis 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, usetext(primary), nottext-muted.
Placeholder text
Per WCAG 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 yourapp.csstoken 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 (focusring,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 — one pair at a time
- Pa11y — 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:
-
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 withhtml:not(.dark)(or your equivalent dark-mode root selector) so they drop out of the cascade in dark mode. -
Per-theme
--color-wk-bg-elevatedvariations. Different theme presets ship different elevated-surface tones (Retro Terminal#1a1a1a, Cupertino#2C2C2E, Defaultoklch(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):
// 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 — 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.