Theme Controller

v0.0.10

v0.0.10StableHeadlessStatelessAccessibleJSApache-2.0
Installation mix mishka.ui.gen.headless theme_controller
Source View Source

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.

heex
<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.

heex
<.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.

Partdata-partState / ARIAPurpose
Rootrootrole="radiogroup" (not in switch mode), aria-label, data-target, data-storage-key, data-theme-value, data-on-changeCarries the hook and its settings.
Optionoptiondata-value, data-checkedA label that wraps its input, so clicking the text picks the theme.
Inputinputtype="radio" or "checkbox", data-value, data-unchecked-value in switch modeThe native control.
Labellabel-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.