Drop it in for a light/dark toggle with none of the usual boilerplate: it writes the dark class on <html>, remembers the choice in localStorage, and reacts to the OS when set to follow the system. Tailwind's dark mode reads that dark class, so the whole page (and every kit component) re-themes.
<c-ui.mode-toggle />
{% cotton ui.mode-toggle /%}
To stop a flash of the wrong theme before your CSS loads, add the head snippet as the first thing in <head>. It runs a tiny blocking script that sets the dark class before the first paint. Give it the same storage_key and default as your toggle.
<head>
<c-ui.mode-toggle.head />
<link rel="stylesheet" href="...">
</head>
Prefer a toggle that reads as a switch? Set variant="switch" for a sliding pill whose thumb carries the current icon. Same engine underneath, so it stays in sync with every other control on the page.
<c-ui.mode-toggle variant="switch" />
{% cotton ui.mode-toggle variant="switch" /%}
Set default to control what a first-time visitor (no stored choice) sees. "system" follows the OS; use "dark" or "light" to force a starting scheme instead. Keep it in sync with the head snippet.
<c-ui.mode-toggle.head default="dark" />
<c-ui.mode-toggle variant="menu" default="dark" />
{% cotton ui.mode-toggle.head default="dark" /%}
{% cotton ui.mode-toggle variant="menu" default="dark" /%}
The button ships inline sun/moon icons. Override them with the light and dark slots (and system_icon for the menu's System row). Pass any SVG, or pull a ready-made icon from a set like cotton-icons (<c-heroicon.sun />).
{# any SVG, or an icon component from cotton-icons #}
<c-ui.mode-toggle>
<c-slot name="light"><c-heroicon.sun class="size-5" /></c-slot>
<c-slot name="dark"><c-heroicon.moon class="size-5" /></c-slot>
</c-ui.mode-toggle>
{# any SVG, or an icon component from cotton-icons #}
{% cotton ui.mode-toggle %}
{% cotton:slot light %}{% cotton heroicon.sun class="size-5" /%}{% endcotton:slot %}
{% cotton:slot dark %}{% cotton heroicon.moon class="size-5" /%}{% endcotton:slot %}
{% endcotton %}
Need a structure the built-in variants don't cover? Use variant="headless": the component renders only the state engine and drops your markup inside its scope. Any element in the slot can call set('light'), set('dark'), set('system') or toggle(), and read mode and isDark for styling. Persistence, the dark class and cross-control sync all still work. The example below rebuilds the menu variant by hand to show the moving parts.
<c-ui.mode-toggle variant="headless">
{# your markup; set()/mode/isDark come from the component #}
<div x-data="{ open: false }" @click.outside="open = false" class="relative">
<button @click="open = !open">Theme</button>
<div x-show="open" x-cloak>
<button @click="set('light'); open = false"
:class="mode === 'light' && 'font-medium'">Light</button>
<button @click="set('dark'); open = false"
:class="mode === 'dark' && 'font-medium'">Dark</button>
<button @click="set('system'); open = false"
:class="mode === 'system' && 'font-medium'">Auto</button>
</div>
</div>
</c-ui.mode-toggle>
{% cotton ui.mode-toggle variant="headless" %}
{# your markup; set()/mode/isDark come from the component #}
<div x-data="{ open: false }" @click.outside="open = false" class="relative">
<button @click="open = !open">Theme</button>
<div x-show="open" x-cloak>
<button @click="set('light'); open = false"
:class="mode === 'light' && 'font-medium'">Light</button>
<button @click="set('dark'); open = false"
:class="mode === 'dark' && 'font-medium'">Dark</button>
<button @click="set('system'); open = false"
:class="mode === 'system' && 'font-medium'">Auto</button>
</div>
</div>
{% endcotton %}
| Name | Description | Type | Options | Default |
|---|---|---|---|---|
variant |
button: two-way icon button; switch: sliding pill; menu: light/dark/system dropdown; headless: render only the engine and supply your own UI in the slot | str |
buttonswitchmenuheadless
|
button |
:system |
Include the System row in the menu variant (ignored by button and switch) | bool |
TrueFalse
|
True |
default |
Scheme used before the visitor makes a choice; 'system' follows the OS | str |
systemdarklight
|
system |
storage_key |
localStorage key the choice is saved under (match the head snippet) | str | — | theme |
size |
Control size (button and menu variants) | str |
smmdlg
|
md |
class |
Extra classes on the control | str | — |
| Name | Description | Type | Options | Default |
|---|---|---|---|---|
light |
Icon shown in light mode (defaults to a sun) | slot | — | |
dark |
Icon shown in dark mode (defaults to a moon) | slot | — | |
system_icon |
Icon for the System row in the menu variant | slot | — |