Radio Group

v0.0.10

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

Overview of the Phoenix and LiveView Headless Radio Group Component

The Mishka Chelekom Headless Radio Group is a single-select group for Phoenix LiveView with no styling of its own. It renders a role="radiogroup" container of <button role="radio"> items with roving tabindex: the arrow keys move the focus and the selection, and Home and End jump to the first and last enabled option. Its RadioGroup hook keeps data-checked and data-unchecked on every item as the selection changes.

The group takes class and every item takes item_class, so you style both with your own Tailwind classes. orientation sets which way the options run, as data-orientation on the root.

Base UI Radio Group example (Phoenix headless)

Base UI Radio Group example (Phoenix headless)

heex
<.radio_group
  id="baseui-radio-group-hero"
  name="baseui-radio-group-hero"
  value="pro"
  class="flex w-72 flex-col gap-2"
  item_class="flex items-center gap-2 rounded-md border border-neutral-950 bg-white px-3 py-2 text-left text-sm text-neutral-950 before:size-4 before:shrink-0 before:rounded-full before:border before:border-neutral-950 before:content-[''] data-[checked]:before:border-4 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-950 dark:border-white dark:bg-neutral-950 dark:text-white dark:before:border-white dark:focus-visible:outline-white"
>
  <:option value="free">Free</:option>
  <:option value="pro">Pro</:option>
  <:option value="enterprise">Enterprise</:option>
  <:option value="legacy" disabled>Legacy (unavailable)</:option>
</.radio_group>

Radio Group example - horizontal (Phoenix headless)

With orientation="horizontal" the root carries data-orientation="horizontal", which the classes below lay out as a row that wraps on a narrow screen. The arrow keys work the same in both orientations: Down and Right go to the next option, Up and Left to the previous one.

Radio Group example - horizontal (Phoenix headless)

heex
<.radio_group
  id="size-picker"
  name="size"
  value={@size}
  orientation="horizontal"
  on_change="pick_size"
  class="flex flex-col gap-2 data-[orientation=horizontal]:flex-row data-[orientation=horizontal]:flex-wrap"
  item_class="flex min-w-12 items-center justify-center rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm font-medium text-neutral-700 data-[checked]:border-neutral-950 data-[checked]:bg-neutral-950 data-[checked]:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-950 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:data-[checked]:border-white dark:data-[checked]:bg-white dark:data-[checked]:text-neutral-950 dark:focus-visible:outline-white"
>
  <:option value="s">S</:option>
  <:option value="m">M</:option>
  <:option value="l">L</:option>
  <:option value="xl">XL</:option>
</.radio_group>
elixir
def handle_event("pick_size", %{"value" => size}, socket) do
  {:noreply, assign(socket, :size, size)}
end

Attributes and slots

id is required. value is the selected option's value. orientation is "vertical" (the default) or "horizontal". disabled disables the whole group and sets data-disabled and aria-disabled. readonly keeps the focus moving but blocks a new selection, and sets aria-readonly. required sets aria-required and required on the hidden input.

Give the group a name to render a hidden input that holds the selected value. The hook writes the new value into it and fires an input event, so an enclosing <.form phx-change> reacts. form names the form the input belongs to when the group is rendered outside it, and on_change is a LiveView event pushed with %{"value" => value} on every selection.

Each option slot takes a required value and an optional disabled. A disabled option is skipped by the arrow keys and carries data-disabled and aria-disabled.

Parts & data attributes

Each item carries a stable data-part and a structural chelekom-radio-group__* class you can target, plus the state the RadioGroup hook keeps in sync.

Partdata-partState / ARIAPurpose
Root(the group div)role="radiogroup", aria-disabled, aria-readonly, aria-required, data-orientation, data-disabledThe container; takes class and holds the RadioGroup hook.
Itemitemrole="radio", aria-checked, aria-disabled, data-checked, data-unchecked, data-disabledA selectable option; takes item_class. Only the checked (or first enabled) item is tabbable.
Hidden input(rendered when name is set)type="hidden", requiredCarries the selected value so the group submits with a form.

Accessibility

Follows the WAI-ARIA Radio Group pattern: only one item is in the tab sequence at a time (roving tabindex), Arrow Down/Right selects the next option, Arrow Up/Left the previous one (wrapping), Home / End jump to the first/last enabled option, and disabled items are skipped entirely. Give the group an accessible name, for example with aria-label or aria-labelledby pointing at a visible heading.