Theme Controller
Every WireKit app needs a dark-mode toggle, and every one of them hand-rolls the same three things: the class switch, the persistence, and the head script that stops the page flashing white before it turns dark.
This is those three things.
Setup
Two steps, and the first one is not optional.
{{-- 1. In your layout's <head>, BEFORE any stylesheet.
This applies the reader's stored theme before the first paint. --}}
<head>
@wirekitThemeScript
@wirekitStyles
</head>
{{-- 2. Put a control wherever it belongs. --}}
<x-wirekit::theme-controller />
That is the whole integration. No Alpine store, no localStorage plumbing, no
document.documentElement.classList in your own code.
The head script must be inline, and it must come first
@wirekitThemeScript is a synchronous inline script on purpose. Anything deferred
or external runs after the browser has painted — so the reader sees a white page
that then turns dark. That flash is the whole reason the directive exists, and
moving it below your stylesheet or into your bundle brings it straight back.
If you run a Content Security Policy, pass your nonce: @wirekitThemeScript($nonce).
Variants
variant |
Shape | Use it when |
|---|---|---|
button |
An icon button that flips light/dark | A header or toolbar (the default) |
switch |
A labeled switch | A settings row |
select |
System / Light / Dark | You want the reader to be able to say system |
<x-wirekit::theme-controller variant="switch" label="Dark mode" />
System is a real answer
With nothing stored, the page follows the reader's operating system — a first visit should look like the rest of their machine, not like our default. And it keeps following: if their Mac turns dark at sunset, so does the page, live.
The moment they pick light or dark explicitly, that wins and it is remembered.
Only variant="select" can take them back to system. That is the honest
limitation of a two-state control: it has to freeze the reader onto an explicit
choice the instant they touch it, and after that their sunset leaves your page
behind. If that matters for your app, use the select.
Reacting to a theme change
The control dispatches an event, so anything with its own colors — a chart, a map, a third-party embed — can follow along instead of polling for a class:
<div x-data x-on:wirekit:theme-changed.window="chart.setTheme($event.detail.dark ? 'dark' : 'light')">
…
</div>
| Detail | Type | Description |
|---|---|---|
theme |
string |
system, light or dark — what the reader chose. |
dark |
bool |
Whether the page is dark right now, whatever the reason. |
Where the choice is stored
In localStorage, under wirekit-theme. Change it in config/wirekit.php:
'theme' => [
'storage_key' => 'my-app-theme',
],
The control and the head script both read that key. They must agree — if they disagree, the page paints one theme and then switches to the other in front of the reader.
Choosing system removes the key rather than storing the word: no key already means "follow the OS", and one meaning in one place is worth more than a value that would be handled identically anyway.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
string |
'button' |
button, switch, select. |
label |
string |
'Dark mode' |
Accessible name. The button variant has no visible text, so this is all a screen reader gets. |
scope |
string|null |
null |
Class-scope override. |
Accessibility
- The button variant carries
aria-pressed: "the page is dark" is a state, and a reader who cannot see the icon still needs to know which way the switch is thrown. The icon alone would be shape-and-color only. - The switch variant is a real
<input type="checkbox" role="switch">— focusable, toggles on Space, announces its own state. A styled<div>would have to reimplement all three. - The select is a native
<select>, so it gets the platform picker on a phone. - The sun/moon swap hides the inactive icon from assistive tech, so a toggle announces one state rather than both.
- If
localStorageis unavailable — private mode, storage disabled — the control still works for the current page; the choice simply does not survive a reload. The head script fails the same way, into the OS preference, rather than taking the page down before it renders.
Keyboard Interaction
| Key | Action |
|---|---|
| Tab | Move focus to the control. |
| Space | Toggle the button and the switch variants. |
| ↑ ↓ | Change the selection in the select variant (native behavior). |