Segmented Control

v0.0.10

v0.0.10StableHeadlessStatelessAccessibleApache-2.0
Installation mix mishka.ui.gen.headless segmented_control
Source View Source

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)

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

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

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

heex
<.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>
elixir
def handle_event("pick", %{"view_form" => %{"view" => view}}, socket) do
  {:noreply, assign(socket, value: view)}
end

Parts & data attributes

Each part has a data-part value and a chelekom-segmented-control__* class.

  • root is the radiogroup. It takes class and has data-disabled when the whole control is disabled.
  • item is the label around one segment. It takes item_class and has data-disabled when that segment or the whole control is disabled.
  • input is the native radio. It takes input_class. Hide it visually, never with display: none, so it stays focusable.
  • label is the visible text of the segment. It takes label_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).