Switch
v0.0.10
mix mishka.ui.gen.headless switch
Overview of the Phoenix and LiveView Headless Switch Component
The Mishka Chelekom Headless Switch is an on/off control for Phoenix LiveView with no styling of its own. It renders a <button role="switch"> with the WAI-ARIA wiring and its state as data attributes: data-checked or data-unchecked, and data-indeterminate for the third state a "select all" switch needs. Its Toggle hook keeps the state on the root, the track and the thumb, so each of them can be styled from its own attributes.
Every part takes a *_class attribute: class for the root, track_class, thumb_class, label_class, input_class, and on_icon_class and off_icon_class for the two optional icons inside the track.
Base UI Switch example (Phoenix headless)
Base UI Switch example (Phoenix headless)
<.switch
id="baseui-switch-hero"
checked
class="group inline-flex items-center gap-2 text-sm font-normal text-neutral-950 select-none dark:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-950 dark:focus-visible:outline-white"
track_class="flex h-5 w-9 shrink-0 items-center border border-neutral-950 bg-white p-0.5 transition-colors duration-150 ease-[ease] dark:border-white dark:bg-neutral-950 group-data-[checked]:bg-neutral-950 dark:group-data-[checked]:bg-white"
thumb_class="block size-3.5 bg-neutral-950 transition-[translate,background-color] duration-150 ease-[ease] data-[checked]:translate-x-4 data-[checked]:bg-white dark:bg-white dark:data-[checked]:bg-neutral-950"
>
Notifications
</.switch>Switch example - icons in the track (Phoenix headless)
The on_icon and off_icon slots are drawn inside the track, after the thumb, each in a span of its own. Neither is drawn when its slot is empty. Here the track is the named group group/track, and each icon fades in or out with group-data-[checked]/track:.
Switch example - icons in the track (Phoenix headless)
<.switch
id="switch-icons"
checked
class="group inline-flex items-center gap-2 text-sm text-neutral-950 select-none dark:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-950 dark:focus-visible:outline-white"
track_class="group/track relative flex h-6 w-11 shrink-0 items-center rounded-full bg-neutral-300 p-0.5 transition-colors duration-150 data-[checked]:bg-neutral-950 dark:bg-neutral-700 dark:data-[checked]:bg-white"
thumb_class="relative z-10 block size-5 rounded-full bg-white shadow-sm transition-[translate] duration-150 data-[checked]:translate-x-5 dark:bg-neutral-950"
on_icon_class="absolute left-1.5 flex text-white opacity-0 transition-opacity duration-150 group-data-[checked]/track:opacity-100 dark:text-neutral-950"
off_icon_class="absolute right-1.5 flex text-neutral-600 opacity-100 transition-opacity duration-150 group-data-[checked]/track:opacity-0 dark:text-neutral-300"
>
<:on_icon><.icon name="hero-check-mini" class="size-3.5" /></:on_icon>
<:off_icon><.icon name="hero-x-mark-mini" class="size-3.5" /></:off_icon>
Wi-Fi
</.switch>Switch example - indeterminate (Phoenix headless)
Set indeterminate when the switch stands for a group that is only partly on, such as a "select all" row. The root reads aria-checked="mixed", and the root, track and thumb carry data-indeterminate instead of data-checked or data-unchecked. The state is rendered on the server, so it is right on the first paint. The next press resolves it to on.
Switch example - indeterminate (Phoenix headless)
<.switch
id="switch-select-all"
checked={@selected != [] and length(@selected) == length(@items)}
indeterminate={@selected != [] and length(@selected) < length(@items)}
on_change="toggle_all"
class="group inline-flex items-center gap-2 text-sm text-neutral-950 select-none dark:text-white"
track_class="flex h-6 w-11 shrink-0 items-center rounded-full bg-neutral-300 p-0.5 transition-colors duration-150 data-[checked]:bg-neutral-950 data-[indeterminate]:bg-neutral-500 dark:bg-neutral-700 dark:data-[checked]:bg-white"
thumb_class="block size-5 rounded-full bg-white shadow-sm transition-[translate] duration-150 data-[checked]:translate-x-5 data-[indeterminate]:translate-x-2.5 dark:bg-neutral-950"
>
Select all
</.switch>def handle_event("toggle_all", %{"checked" => checked}, socket) do
selected = if checked, do: socket.assigns.items, else: []
{:noreply, assign(socket, :selected, selected)}
endAttributes and slots
id is required: the label's id and aria-labelledby are built from it. checked sets the state on render, and indeterminate takes over from it while it is true. disabled ignores every press and sets data-disabled. readonly blocks the toggle but keeps the switch focusable, and sets aria-readonly. required sets aria-required, and required on the hidden checkbox.
For a form, give the switch a name. It then renders a hidden checkbox that submits value (default "on") while the switch is on. unchecked_value adds a second hidden input, so a value is submitted while it is off too, and form names the form the inputs belong to when the switch is rendered outside it. on_change is a LiveView event pushed on every toggle with %{"checked" => true} or %{"checked" => false}.
The default slot is the label. on_icon and off_icon are optional icons inside the track.
Parts & data attributes
Each part carries a stable data-part and a structural chelekom-switch__* class you can target.
| Part | data-part | State / ARIA | Purpose |
|---|---|---|---|
| Root | (button) | role="switch", aria-checked (true, false or mixed), aria-labelledby, aria-readonly, aria-required, data-checked, data-unchecked, data-indeterminate, data-disabled, data-readonly, data-required | The control itself. |
| Input | input | aria-hidden | Hidden checkbox that submits the value; rendered when the switch has a name. |
| Track | track | aria-hidden, data-checked, data-unchecked, data-indeterminate | The box the thumb slides in; the hook keeps its state. |
| Thumb | thumb | aria-hidden, data-checked, data-unchecked, data-indeterminate | The sliding dot; the hook keeps its state. |
| On icon | on-icon | aria-hidden; the state of the first render | The on_icon slot, inside the track after the thumb. |
| Off icon | off-icon | aria-hidden; the state of the first render | The off_icon slot, inside the track after the thumb. |
| Label | label | - | Wraps the default slot; the root is labelled by it. |
Accessibility
Follows the WAI-ARIA Switch pattern: the root exposes role="switch" with a live aria-checked (true, false, or mixed while indeterminate), and Enter or Space toggles the state. readonly blocks the flip but keeps the control focusable, while disabled ignores interaction entirely. The icons are aria-hidden, so give the switch a text label.
Attributes
| Name | Type | Default | Values | Required | Description |
|---|---|---|---|---|---|
id
|
string
| — | — | Yes | Unique id (anchors aria relationships) |
name
|
string
| — | — | No | Name for the hidden form input |
checked
|
boolean
|
false
| — | No | Initial/controlled on state |
indeterminate
|
boolean
|
false
| — | No | Neither on nor off (aria-checked="mixed"); the next toggle resolves it to on |
disabled
|
boolean
|
false
| — | No | Ignore interaction (data-disabled) |
readonly
|
boolean
|
false
| — | No | Block toggling but stay focusable (data-readonly) |
required
|
boolean
|
false
| — | No | Require the switch on for form submit (data-required) |
value
|
string
|
on
| — | No | Value submitted when on |
unchecked_value
|
string
| — | — | No | Value submitted when off (adds a companion hidden input) |
form
|
string
| — | — | No | Form id owning the hidden input (when rendered outside the form) |
on_change
|
string
| — | — | No | LiveView event pushed on toggle ({checked}) |
class
|
any
| — | — | No | Extra classes for the root (flex wrapper) |
track_class
|
any
| — | — | No | Extra classes for the track part (the switch box) |
thumb_class
|
any
| — | — | No | Extra classes for the thumb part (the sliding dot) |
label_class
|
any
| — | — | No | Extra classes for the label part |
input_class
|
any
| — | — | No | — |
off_icon_class
|
any
| — | — | No | — |
on_icon_class
|
any
| — | — | No | — |
rest
|
global
| — | — | No | — |
Slots
| Name | Required | Description |
|---|---|---|
on_icon
| No | Optional icon rendered inside the track, shown while on |
off_icon
| No | Optional icon rendered inside the track, shown while off |
inner_block
| No | Optional label content |
Options
Each option takes one or more of its values, separated by commas.
| Option | Values |
|---|---|
--type
|
switch
|
--module
| A module name of your own |
What it generates
mix mishka.ui.gen.headless switch