Theme Controller
v0.0.10
mix mishka.ui.gen.headless theme_controller
Overview of the Phoenix and LiveView Headless Theme Controller Component
The Mishka Chelekom Headless Theme Controller is a Phoenix function component that lets people pick a theme in a LiveView page and keeps the choice. It writes data-theme onto a target element, stores the choice in localStorage and restores it on the next visit. The controls are native inputs: one radio per theme, or a single checkbox in switch mode. Keyboard navigation, form behavior and screen reader semantics therefore come from the browser. The ThemeController hook only listens for change and applies the result.
target says which element gets data-theme. The default, :root, is the page, and any CSS selector works. The system attribute adds an option that follows prefers-color-scheme. The parts are option (a label), input and label (a span with the text), styled with option_class, input_class and label_class. class styles the root.
Base UI Theme Controller example - light and dark on a preview box (Phoenix headless)
Base UI Theme Controller example - light and dark on a preview box (Phoenix headless)
This box follows the choice above.
<div class="theme-demo-box flex w-72 flex-col items-center gap-3 rounded-lg border border-neutral-200 bg-white p-6 text-neutral-950 data-[theme=dark]:border-neutral-800 data-[theme=dark]:bg-neutral-950 data-[theme=dark]:text-white">
<.theme_controller
id="baseui-theme-hero"
target=".theme-demo-box"
value="light"
class="flex items-center gap-4 text-sm"
option_class="flex items-center gap-1.5"
>
<:option value="light">Light</:option>
<:option value="dark">Dark</:option>
</.theme_controller>
<p class="text-xs text-neutral-500">This box follows the choice above.</p>
</div>Attributes, slots and events
id is required because it carries the hook. name is the name of the inputs and storage_key is the localStorage key; both default to id. Set storage_key to an empty string to stop remembering the choice. label is the accessible name of the group and defaults to "Theme".
value is the theme the server renders as selected. Once the hook runs, a theme saved in the browser wins over it, because it is the more recent choice. The hook applies the saved theme after the page loads, so a visitor with a saved theme can see the default one for a moment first.
Each :option entry takes a required value, the name written to data-theme, plus an optional label and class. Content inside the entry replaces the visible text, so an icon or a color dot can stand in for the name. on_change is a LiveView event name. The hook pushes it with %{"theme" => "dark"} when the visitor picks a theme.
Switch and system options
switch draws one checkbox in place of a radio per theme. Unchecked means the first option, checked means the second, and only the second option's content is drawn, so use it with exactly two themes. system adds a last option, named by system_label ("System" by default). Choosing it writes light or dark to data-theme from the operating system's prefers-color-scheme, and updates it when that setting changes.
wrap_label is true by default and wraps the content of each option in a label part. Set it to false to put the content directly beside the input, for example when an icon has to be a sibling of the input so CSS can style it from the input's state.
<.theme_controller id="theme-switch" switch value="light">
<:option value="light">Light</:option>
<:option value="dark">Dark</:option>
</.theme_controller>
<.theme_controller id="theme-radios" system system_label="Auto" value="light">
<:option value="light">Light</:option>
<:option value="dark">Dark</:option>
</.theme_controller>Parts & data attributes
Each part carries a stable data-part and a structural chelekom-theme-controller* class you can target.
| Part | data-part | State / ARIA | Purpose |
|---|---|---|---|
| Root | root | role="radiogroup" (not in switch mode), aria-label, data-target, data-storage-key, data-theme-value, data-on-change | Carries the hook and its settings. |
| Option | option | data-value, data-checked | A label that wraps its input, so clicking the text picks the theme. |
| Input | input | type="radio" or "checkbox", data-value, data-unchecked-value in switch mode | The native control. |
| Label | label | - | The option content, or the theme name. Not drawn when wrap_label is false. |
Accessibility
Built on native inputs, so the browser supplies the behavior of the WAI-ARIA Radio Group pattern: Tab reaches the group, the arrow keys move between themes and Space picks the focused one. The root has role="radiogroup" and is named by label. In switch mode there is a single checkbox and no group role. Each input sits inside its option label, so clicking the text picks the theme. Give the inputs a visible focus style.
Attributes
| Name | Type | Default | Values | Required | Description |
|---|---|---|---|---|---|
id
|
string
| — | — | Yes | Unique id (carries the ThemeController hook) |
name
|
string
| — | — | No | Name for the inputs; defaults to the id |
value
|
string
| — | — | No | The theme selected on the server; the stored choice wins once the hook runs |
target
|
string
|
:root
| — | No | — |
storage_key
|
string
| — | — | No | localStorage key; defaults to the id. Set to "" to not persist at all |
label
|
string
|
Theme
| — | No | Accessible name for the group |
switch
|
boolean
|
false
| — | No | Render a single checkbox between the first two themes instead of a radio per theme |
system_label
|
string
|
System
| — | No | Label for the `system` option |
system
|
boolean
|
false
| — | No | Add an option that follows the OS setting, and keeps following it |
on_change
|
string
| — | — | No | LiveView event pushed as `%{theme}` on every change |
class
|
any
| — | — | No | Extra classes for the root |
option_class
|
any
| — | — | No | — |
input_class
|
any
| — | — | No | — |
label_class
|
any
| — | — | No | — |
wrap_label
|
boolean
|
true
| — | No | Wrap each option's content in a `label` part. Set false when the content has to sit directly beside the input — daisyUI's swap and toggle-with-icons recipes both place their glyphs as siblings of it, and address them with `~` and `:nth-child` |
rest
|
global
| — | — | No | — |
Slots
| Name | Required | Description |
|---|---|---|
option
| No | One theme |
Attributes of the option slot
| Name | Type | Default | Values | Required | Description |
|---|---|---|---|---|---|
value
|
string
| — | — | Yes | The theme name written to `data-theme` |
label
|
string
| — | — | No | Visible text; falls back to the value |
class
|
any
| — | — | No | — |
Options
Each option takes one or more of its values, separated by commas.
| Option | Values |
|---|---|
--type
|
theme_controller
|
--module
| A module name of your own |
What it generates
mix mishka.ui.gen.headless theme_controller