Segmented Control
v0.0.10
mix mishka.ui.gen.headless segmented_control
Overview of the Phoenix and LiveView Headless Segmented Control Component
The Mishka Chelekom Headless Segmented Control is a row of mutually exclusive options for Phoenix and LiveView forms, built on native radio inputs. It needs no JavaScript. The radios share a name, so selection, form submission and Arrow-key movement are the browser's own. The root is a div with role="radiogroup", and each segment is a label holding a radio and a span for the text.
options is a list of values, {label, value} tuples or {label, value, disabled} tuples. value is the selected one, label becomes the group's aria-label, and disabled disables every segment. Hide the radios with sr-only through input_class and draw the selected segment with the CSS :has(:checked) selector, for example Tailwind's has-[:checked]: variants in item_class. The parts take class, item_class, input_class and label_class.
Base UI Segmented Control example - framework switcher (Phoenix headless)
Base UI Segmented Control example - framework switcher (Phoenix headless)
<.segmented_control
id="baseui-segmented-control"
name="baseui-sc"
value="React"
options={["React", "Vue", "Svelte"]}
label="Framework"
class="inline-flex max-w-full rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800"
item_class="cursor-pointer rounded-md px-3 py-1 text-sm text-neutral-600 has-[:checked]:bg-white has-[:checked]:font-medium has-[:checked]:text-neutral-900 has-[:checked]:shadow-sm has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-neutral-400 dark:text-neutral-400 dark:has-[:checked]:bg-neutral-950 dark:has-[:checked]:text-white"
input_class="sr-only"
/>Segmented Control example - one disabled segment (Phoenix headless)
To disable one segment, give its option a third tuple element: {"Table", "table", true}. The segment gets data-disabled and a disabled radio, so it cannot be chosen or focused, and the others keep working. A content marker can only write strings, so this example has no live preview.
<.segmented_control
id="view-switch"
name="view"
value="list"
options={[{"List", "list"}, {"Grid", "grid"}, {"Table", "table", true}]}
label="View"
class="inline-flex max-w-full rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800"
item_class="cursor-pointer rounded-md px-3 py-1 text-sm text-neutral-600 has-[:checked]:bg-white has-[:checked]:font-medium has-[:checked]:text-neutral-900 has-[:checked]:shadow-sm has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-neutral-400 dark:text-neutral-400 dark:has-[:checked]:bg-neutral-950 dark:has-[:checked]:text-white data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50"
input_class="sr-only"
/>Segmented Control example - disabled control (Phoenix headless)
disabled on the control disables every segment and sets data-disabled on the root and on each segment, so one data-[disabled]: variant dims them all.
Segmented Control example - disabled control (Phoenix headless)
<.segmented_control
id="view-disabled"
name="view-disabled"
value="List"
options={["List", "Grid", "Table"]}
label="View"
disabled
class="inline-flex max-w-full rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800"
item_class="cursor-pointer rounded-md px-3 py-1 text-sm text-neutral-600 has-[:checked]:bg-white has-[:checked]:font-medium has-[:checked]:text-neutral-900 has-[:checked]:shadow-sm has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-neutral-400 dark:text-neutral-400 dark:has-[:checked]:bg-neutral-950 dark:has-[:checked]:text-white data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50"
input_class="sr-only"
/>Using it in a form
The segments are real radios, so the choice posts with the form under name. Radios with the same name in the same form are one group, so give each control its own name. Put the control inside a form with phx-change and the server hears every change.
<.form for={to_form(%{"view" => @value}, as: :view_form)} id="view-form" phx-change="pick">
<.segmented_control
id="view-control"
name="view_form[view]"
value={@value}
options={[{"Grid", "grid"}, {"List", "list"}, {"Table", "table"}]}
label="View"
class="inline-flex max-w-full rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800"
item_class="cursor-pointer rounded-md px-3 py-1 text-sm text-neutral-600 has-[:checked]:bg-white has-[:checked]:font-medium has-[:checked]:text-neutral-900 has-[:checked]:shadow-sm has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-neutral-400 dark:text-neutral-400 dark:has-[:checked]:bg-neutral-950 dark:has-[:checked]:text-white"
input_class="sr-only"
/>
</.form>def handle_event("pick", %{"view_form" => %{"view" => view}}, socket) do
{:noreply, assign(socket, value: view)}
endParts & data attributes
Each part has a data-part value and a chelekom-segmented-control__* class.
rootis theradiogroup. It takesclassand hasdata-disabledwhen the whole control is disabled.itemis thelabelaround one segment. It takesitem_classand hasdata-disabledwhen that segment or the whole control is disabled.inputis the native radio. It takesinput_class. Hide it visually, never withdisplay: none, so it stays focusable.labelis the visible text of the segment. It takeslabel_class.
Accessibility
Follows the WAI-ARIA Radio Group pattern through native radios. Tab moves into and out of the group, and the Arrow keys move the selection. The radios are hidden only visually, so give the selected and focused segments a visible style, as the examples do with :has(:checked) and :has(:focus-visible).
Attributes
| Name | Type | Default | Values | Required | Description |
|---|---|---|---|---|---|
id
|
string
| — | — | No | Optional unique id |
name
|
string
| — | — | No | Form field name (shared across segments) |
value
|
any
| — | — | No | Selected value |
options
|
list
| — | — | Yes | List of values (strings), {label, value} or {label, value, disabled} tuples |
label
|
string
| — | — | No | Accessible group label (aria-label) |
disabled
|
boolean
|
false
| — | No | Disable the whole control |
class
|
any
| — | — | No | Extra classes for the root |
item_class
|
any
| — | — | No | Extra classes for each segment |
input_class
|
any
| — | — | No | Extra classes for the hidden radios (e.g. sr-only) |
label_class
|
any
| — | — | No | — |
rest
|
global
| — | — | No | — |
Options
Each option takes one or more of its values, separated by commas.
| Option | Values |
|---|---|
--type
|
segmented_control
|
--module
| A module name of your own |
What it generates
mix mishka.ui.gen.headless segmented_control