Mutually-exclusive radio button group component for Phoenix LiveView.
Renders a group of radio buttons with custom circular indicators styled with
Tailwind semantic tokens. All inputs are native <input type="radio"> elements,
so keyboard navigation (arrow keys), screen readers, and browser autofill work
out-of-the-box without any JavaScript.
Sub-components
| Function | Purpose |
|---|---|
radio_group/1 | Container with role="radiogroup", exposes context via :let |
radio_group_item/1 | Individual styled radio option with a custom circular indicator |
form_radio_group/1 | Full group integrated with Phoenix.HTML.FormField and errors |
Standalone usage with :let context
The :let pattern passes the group's shared name and current value down
to each item automatically, avoiding repetition:
<.radio_group :let={group} value={@plan} name="plan" phx-change="set_plan">
<.radio_group_item value="free" label="Free" {group} />
<.radio_group_item value="pro" label="Pro" {group} />
<.radio_group_item value="enterprise" label="Enterprise" {group} />
</.radio_group>The {group} spread assigns name={group.name} and group_value={group.group_value}
to each item so they know which one is currently selected.
Horizontal layout
<.radio_group :let={group} value={@size} name="size" orientation="horizontal">
<.radio_group_item value="xs" label="XS" {group} />
<.radio_group_item value="sm" label="SM" {group} />
<.radio_group_item value="md" label="MD" {group} />
<.radio_group_item value="lg" label="LG" {group} />
</.radio_group>Form-integrated usage
form_radio_group/1 is the simplest API for Ecto changeset forms. Pass an
options list and the component handles name, value, and error display:
<.form_radio_group
field={@form[:plan]}
label="Subscription plan"
options={[{"Free", "free"}, {"Pro", "pro"}, {"Team", "team"}]}
/>
<.form_radio_group
field={@form[:experience_level]}
label="Experience level"
orientation="horizontal"
options={[{"Beginner", "beginner"}, {"Intermediate", "intermediate"}, {"Advanced", "advanced"}]}
/>Settings form example
<.form for={@form} phx-submit="save_preferences">
<.form_radio_group
field={@form[:theme]}
label="Color theme"
orientation="horizontal"
options={[{"Light", "light"}, {"Dark", "dark"}, {"System", "system"}]}
/>
<.form_radio_group
field={@form[:notifications]}
label="Notification frequency"
options={[
{"Real-time", "realtime"},
{"Hourly digest", "hourly"},
{"Daily digest", "daily"},
{"Never", "never"}
]}
/>
<.button type="submit">Save preferences</.button>
</.form>Accessibility
role="radiogroup"on the container groups items for screen readers- Native
<input type="radio">withclass="sr-only"handles keyboard navigation (arrow keys cycle through options) and screen reader announcements - Labels are associated via
for/idso clicking the text selects the option peer-focus-visible:ring-2on the visual indicator provides keyboard focus feedback- Disabled items have
aria-disabledsemantics via the nativedisabledattribute
Summary
Functions
Renders a complete radio group integrated with Phoenix.HTML.FormField.
Renders a radio button group container with role="radiogroup".
Renders a single radio button item within a radio_group/1.
Functions
Renders a complete radio group integrated with Phoenix.HTML.FormField.
This is the simplest API for Ecto changeset forms. Provide :field and
:options and the component handles everything: deriving the name from
the field, pre-selecting the current value, and displaying changeset errors.
Options are provided as {label, value} tuples. Values are coerced to
strings via to_string/1 before comparison, so integer and atom values
from changesets work transparently.
Examples
<%!-- Subscription plan selector --%>
<.form_radio_group
field={@form[:plan]}
label="Subscription plan"
options={[{"Free", "free"}, {"Pro", "pro"}, {"Team", "team"}]}
/>
<%!-- Horizontal notification preference --%>
<.form_radio_group
field={@form[:notification_frequency]}
label="Notifications"
orientation="horizontal"
options={[
{"Real-time", "realtime"},
{"Daily digest", "daily"},
{"Never", "never"}
]}
/>
<%!-- Dynamic options from the database --%>
<.form_radio_group
field={@form[:category_id]}
label="Category"
options={Enum.map(@categories, &{&1.name, to_string(&1.id)})}
/>Attributes
field(Phoenix.HTML.FormField) (required) - APhoenix.HTML.FormFieldstruct obtained via@form[:field_name]. Provides the groupname, currentvalue(for pre-selection), anderrorsfor changeset validation display.options(:list) - List of{label, value}tuples defining the available radio options. Each tuple becomes aradio_group_item/1. Values are coerced to strings viato_string/1for comparison with the field value.Example:
[{"Free", "free"}, {"Pro", "pro"}, {"Enterprise", "enterprise"}]Defaults to
[].label(:string) - Optional group label rendered as a<p>above the options. Use to describe what the user is choosing. For accessibility, consider wrapping in a<fieldset>with<legend>in complex forms.Defaults to
nil.orientation(:string) - Layout direction forwarded to theradio_group/1container."vertical"(default) stacks options."horizontal"places them inline.Defaults to
"vertical". Must be one of"vertical", or"horizontal".class(:string) - Additional CSS classes applied to the outer wrapper<div>. Defaults tonil.
Renders a radio button group container with role="radiogroup".
Exposes group context via :let so items receive their shared name and
group_value (the currently selected value) automatically. Items use this
context to compute their checked state without needing it passed explicitly:
<.radio_group :let={group} value={@selected} name="fruit" phx-change="select_fruit">
<.radio_group_item value="apple" label="Apple" {group} />
<.radio_group_item value="banana" label="Banana" {group} />
<.radio_group_item value="cherry" label="Cherry" {group} />
</.radio_group>The {group} spread is equivalent to:
name={group.name} group_value={group.group_value}
Examples
<%!-- Vertical (default) --%>
<.radio_group :let={g} value={@color} name="color" phx-change="pick_color">
<.radio_group_item value="red" label="Red" {g} />
<.radio_group_item value="blue" label="Blue" {g} />
</.radio_group>
<%!-- Horizontal --%>
<.radio_group :let={g} value={@size} name="size" orientation="horizontal">
<.radio_group_item value="sm" label="Small" {g} />
<.radio_group_item value="md" label="Medium" {g} />
<.radio_group_item value="lg" label="Large" {g} />
</.radio_group>Attributes
value(:string) - The currently selected value. Theradio_group_item/1whose:valuematches this string will render as checked. Passnilfor an unselected group. This should mirror your LiveView assign (e.g.@selected_plan).Defaults to
nil.name(:string) - The HTMLnameattribute shared by all radio inputs in this group. All radios with the same name are mutually exclusive. Required for form submission. When usingform_radio_group/1, this is derived from the field struct automatically.Defaults to
nil.orientation(:string) - Layout direction for the items."vertical"(default) stacks items in a column withflex-col."horizontal"places them inline withflex-row flex-wrap, which wraps on narrow viewports.Defaults to
"vertical". Must be one of"vertical", or"horizontal".class(:string) - Additional CSS classes applied to the group container<div>. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the group
<div>. Typically used forphx-changeto notify the LiveView when the selection changes. Supports all globals plus:["phx-change", "phx-value"].
Slots
inner_block(required) - One or moreradio_group_item/1components. Use:let={group}on the parent to pass context, then spread{group}onto each item.
Renders a single radio button item within a radio_group/1.
The visible indicator is a custom-styled circle built with CSS. The actual
<input type="radio"> is hidden with class="sr-only" but remains fully
functional for keyboard navigation and form submission. The <label> wraps
both the hidden input and the visual indicator so clicking anywhere on the
row selects the option.
The checked state is computed at render time by comparing group_value
against this item's value. A filled inner circle (<span>) is rendered
conditionally only when checked.
Examples
<%!-- Typical usage inside radio_group with :let context --%>
<.radio_group :let={g} value={@plan} name="plan">
<.radio_group_item value="free" label="Free plan" {g} />
<.radio_group_item value="pro" label="Pro plan" {g} />
</.radio_group>
<%!-- Manual usage without group context --%>
<.radio_group_item
value="monthly"
label="Monthly billing"
name="billing_cycle"
group_value={@billing_cycle}
/>
<%!-- Disabled item --%>
<.radio_group_item value="enterprise" label="Enterprise (contact us)" {g} disabled={true} />Attributes
value(:string) (required) - The value this radio button represents. Submitted to the server when this option is selected. Should match the expected changeset field values.label(:string) (required) - Visible text label rendered next to the custom circular indicator.name(:string) - HTMLnameattribute for this radio input. In normal usage this is inherited from the group context via:let+{group}spread. Only set this manually when composing items without the group container.Defaults to
nil.group_value(:string) - The group's currently selected value. This item renders as checked whengroup_value == value. Inherited from the group context via{group}spread in typical usage. Only set manually when composing without the group.Defaults to
nil.disabled(:boolean) - Whentrue, disables this radio option. The item becomes non-interactive and renders at 50% opacity withcursor-not-allowed.Defaults to
false.class(:string) - Additional CSS classes applied to the<label>wrapper element. Defaults tonil.