Fluxon.Components.Radio
(Fluxon v3.0.0)
Radio group input for selecting a single value from a mutually exclusive set of options.
Renders a list of radio buttons that share a name and submit one value when checked.
Built-in label, sublabel, description, and error rendering wrap the group, and a card
variant turns each option into a clickable surface for richer pickers such as plan
selectors, category choosers, or onboarding paths. Integrates directly with Phoenix
forms via the field attribute.
Choosing between radio and related inputs
| Component | Best suited for |
|---|---|
radio_group/1 | Single selection from a mutually exclusive set of options. |
Fluxon.Components.Checkbox | Boolean opt-in or multi-select from a known list. |
Fluxon.Components.Select | Single selection when the option list is long or space is tight. |
Fluxon.Components.Switch | On/off toggle for an immediately applied setting. |
Usage
The simplest form takes a name, a value, and one :radio slot per option:
<.radio_group name="system" value="debian" label="Operating System">
<:radio value="ubuntu" label="Ubuntu" />
<:radio value="debian" label="Debian" />
<:radio value="fedora" label="Fedora" />
</.radio_group>The group participates in the tab order as a single stop, and the arrow keys move the selection between enabled options, matching native radio behavior.
Add sublabel and description at the group level for context, and on individual
options to expand on each choice:
<.radio_group
name="system"
label="Operating System"
sublabel="Choose your preferred OS"
description="Select the operating system that best suits your needs."
>
<:radio
value="ubuntu"
label="Ubuntu"
sublabel="Popular and user-friendly"
description="Debian-based distribution with a polished desktop experience."
/>
<:radio
value="debian"
label="Debian"
sublabel="Stable and reliable"
description="Composed entirely of free and open-source software."
/>
<:radio
value="fedora"
label="Fedora"
sublabel="Cutting-edge features"
description="Tracks upstream releases closely, ideal for developers."
/>
</.radio_group>Variants
The variant attribute controls the surrounding chrome:
nil(default): Standard stacked radio buttons with label, sublabel, and description. Use for typical single-select form fields."card": Each option becomes a clickable card surface that highlights when selected. Use for richer pickers such as plan selectors, category pickers, or onboarding flows.
In the card variant, control positions the radio input within each card:
"left": Radio leads, label content trails. Default-feeling layout for option lists."right": Radio trails, label content leads. Use when the label carries the primary visual weight (titles, icons, prices).
When control is omitted in the card variant, the radio input is visually hidden
(sr-only) and the entire card surface communicates the checked state. This is the
pattern used by the rich-content example below.
Card variant with stacked options
<.radio_group
name="system"
label="Choose a plan"
description="Choose the plan that best suits your needs."
variant="card"
control="left"
class="gap-0"
>
<:radio
value="basic"
label="Basic"
sublabel="Perfect for small projects"
class="rounded-none -my-px rounded-t-lg"
/>
<:radio
value="pro"
label="Professional"
checked
sublabel="Most popular for growing teams"
class="rounded-none -my-px"
/>
<:radio
value="business"
label="Business"
sublabel="Advanced features for larger teams"
class="rounded-none -my-px"
/>
<:radio
value="enterprise"
label="Enterprise"
sublabel="Custom solutions for organizations"
class="rounded-none -my-px rounded-b-lg"
/>
</.radio_group>Card variant with custom inner content
Pass an inner block to the :radio slot to replace the default label structure with
arbitrary markup. This is the pattern used for icon grids and visual pickers:
<.radio_group name="category" label="Category" variant="card" class="grid grid-cols-3">
<:radio
value="web-design"
class="flex-1 group has-checked:border-blue-500 has-checked:bg-blue-50"
>
<div class="flex flex-col justify-center items-center w-full gap-2">
<.icon name="u-layout-alt-01-duotone" class="size-6 text-zinc-500 group-has-checked:text-blue-500" />
<span class="font-medium text-sm group-has-checked:text-zinc-800">Web Design</span>
</div>
</:radio>
<:radio
value="ui-ux"
class="flex-1 group has-checked:border-blue-500 has-checked:bg-blue-50"
>
<div class="flex flex-col justify-center items-center w-full gap-2">
<.icon name="u-pen-tool-01-duotone" class="size-6 text-zinc-500 group-has-checked:text-blue-500" />
<span class="font-medium text-sm group-has-checked:text-zinc-800">UI/UX Design</span>
</div>
</:radio>
<:radio
value="development"
class="flex-1 group has-checked:border-blue-500 has-checked:bg-blue-50"
>
<div class="flex flex-col justify-center items-center w-full gap-2">
<.icon name="u-laptop-02-duotone" class="size-6 text-zinc-500 group-has-checked:text-blue-500" />
<span class="font-medium text-sm group-has-checked:text-zinc-800">Development</span>
</div>
</:radio>
</.radio_group>Disabled State
Setting disabled on the group disables every option at once and propagates the
disabled style to the labels. Individual options can be disabled independently by
setting disabled on a :radio slot, leaving the rest of the group interactive:
<.radio_group name="tier" value="free" label="Tier">
<:radio value="free" label="Free" />
<:radio value="pro" label="Pro" />
<:radio value="enterprise" label="Enterprise" disabled sublabel="Contact sales" />
</.radio_group>Selected Value Resolution
An option renders as checked when either of these holds:
- The group's
value(orfield.valuewhen bound) stringifies equal to the option'svalue. The comparison usesto_string/1on both sides, so integer values match string values with the same representation (for examplevalue={1}against<:radio value="1" />). - The slot's own
checkedattribute istrue. Use it to seed a default selection when novalueis in scope yet.
Browsers do not submit unchecked radio inputs. To keep the field present in the payload even when nothing is selected, the component renders a hidden input with an empty string value:
<input type="hidden" name="subscription" value="" />Submissions therefore look like one of:
# nothing selected
%{"subscription" => ""}
# one option selected
%{"subscription" => "pro"}Form Integration
The component offers two ways to wire form data: the field attribute for Phoenix form
bindings, or the name attribute for standalone controls.
Using with Phoenix forms (recommended)
Pass a Phoenix.HTML.FormField to field and the component derives id, name, the
current value, and any validation errors automatically. Errors are only shown after
the field has been used (Phoenix.Component.used_input?/1), matching the convention
used across the rest of Fluxon's form inputs.
<.form :let={f} for={@form} phx-change="validate" phx-submit="save">
<.radio_group
field={f[:subscription]}
label="Subscription Plan"
description="Choose your preferred subscription plan."
>
<:radio value="basic" label="Basic Plan" sublabel="$10/month" />
<:radio value="pro" label="Pro Plan" sublabel="$20/month" />
<:radio value="enterprise" label="Enterprise Plan" sublabel="$50/month" />
</.radio_group>
</.form>Passing field also routes validation errors through the configured translator, so
messages rendered beneath the group respect the application's gettext setup.
Using standalone radio groups
When not bound to a form, supply name, manage value yourself, and pass any error
messages explicitly through errors:
<.radio_group
name="theme"
value={@current_theme}
errors={@errors}
label="Theme Selection"
>
<:radio value="light" label="Light Theme" />
<:radio value="dark" label="Dark Theme" />
<:radio value="system" label="System Theme" />
</.radio_group>When to use each approach
Use field when working with changesets, validation messages, nested associations,
or any CRUD form. Use name for standalone selections such as table filters,
page-level preferences, or controls that drive phx-change handlers without a
backing schema.
Form Attribute
The form attribute is forwarded through :rest to every radio input and to the
hidden empty-value input. Use it when the group lives outside the <form> element it
belongs to:
<.radio_group name="plan" value={@plan} label="Plan" form="signup-form">
<:radio value="free" label="Free" />
<:radio value="pro" label="Pro" />
</.radio_group>Examples
Plan picker bound to a changeset:
<.form :let={f} for={@form} phx-submit="subscribe">
<.radio_group
field={f[:plan]}
variant="card"
control="left"
label="Subscription"
description="You can change plans at any time."
>
<:radio value="free" label="Free" sublabel="$0 / month" />
<:radio value="pro" label="Pro" sublabel="$19 / month" />
<:radio value="team" label="Team" sublabel="$49 / month" />
</.radio_group>
<.button type="submit">Continue</.button>
</.form>Theme switcher wired directly to a LiveView event:
<.radio_group
name="theme"
value={@theme}
label="Theme"
phx-change="set_theme"
>
<:radio value="light" label="Light" />
<:radio value="dark" label="Dark" />
<:radio value="system" label="Match system" />
</.radio_group>Visual category picker using the card variant with custom content:
<.radio_group field={f[:category]} variant="card" class="grid grid-cols-3" label="Category">
<:radio value="design" class="has-checked:border-primary">
<div class="flex flex-col items-center gap-2">
<.icon name="u-pen-tool-01-duotone" class="size-6" />
<span class="font-medium">Design</span>
</div>
</:radio>
<:radio value="engineering" class="has-checked:border-primary">
<div class="flex flex-col items-center gap-2">
<.icon name="u-code-square-duotone" class="size-6" />
<span class="font-medium">Engineering</span>
</div>
</:radio>
<:radio value="operations" class="has-checked:border-primary">
<div class="flex flex-col items-center gap-2">
<.icon name="u-settings-04-duotone" class="size-6" />
<span class="font-medium">Operations</span>
</div>
</:radio>
</.radio_group>Filter group with one option pre-selected via slot checked:
<.radio_group name="sort" label="Sort by">
<:radio value="newest" label="Newest" checked />
<:radio value="oldest" label="Oldest" />
<:radio value="popular" label="Most popular" />
</.radio_group>
Summary
Components
Renders a radio group that collects a single selection from a list of options.
Components
Renders a radio group that collects a single selection from a list of options.
Use for mutually exclusive choices that fit inline on the page (typically up to a
handful of options). Each option is declared via a :radio slot entry; the component
renders the surrounding label, description, and error messages, keeps the field
present in the payload via a hidden empty input, and supports both standard and card
layouts. Pair with the field attribute for automatic Phoenix form binding, or use
name plus value for standalone control.
Attributes
id(:any) - Identifier applied to the group. Each:radioslot input derives its ownidby appending its zero-based index to this value (a group withid="plan"produces"plan-0","plan-1", and so on). When omitted, an ID is generated automatically; afieldbinding reusesfield.idinstead.Defaults to
nil.name(:string) - Form field name shared by every radio input in the group. Required whenfieldis not provided.value(:any) - Currently selected value. The option whosevaluestringifies equal to this attribute renders pre-selected. Provided automatically whenfieldis set; defaults tonil(nothing selected) otherwise.label(:string) - Heading rendered above the radio buttons viaFluxon.Components.Form.label/1. Omit to render the group without a heading, for example when the surrounding layout already provides one.Defaults to
nil.sublabel(:string) - Short qualifier rendered alongside the mainlabel(for example "Optional", "Recommended"). Has no effect whenlabelisnil.Defaults to
nil.description(:string) - Longer explanatory copy rendered beneath the label. Use to clarify what the group is for or how the selection will be used.Defaults to
nil.errors(:list) - Error messages to render beneath the group. Populated automatically from validation output whenfieldis supplied and the field has been used; pass explicitly when managing state outside of Phoenix forms.Defaults to
[].class(:any) - Extra classes applied to the inner container that holds the options. Use this to switch the layout from the default stacked column to, for example, a horizontal row, a multi-column grid ("grid grid-cols-3"), or seamless stacked cards ("gap-0").Defaults to
nil.field(Phoenix.HTML.FormField) - Phoenix form field to bind to. When provided, the component derivesid,name,value, anderrorsfrom the field and runs error messages through the configured translator.disabled(:boolean) - Disables every radio input in the group whentrue. Individual options can still be disabled via the:radioslot's owndisabledattribute regardless of this value.Defaults to
false.variant(:string) - Visual chrome wrapping each option.nil(default): Standard stacked radio buttons with label, sublabel, and description. Use for typical single-select form fields."card": Each option becomes a clickable card surface that highlights when selected. Use for richer pickers such as plan selectors, category pickers, or onboarding flows.
Defaults to
nil.control(:string) - Position of the radio input within each card. Only meaningful whenvariant="card"."left": Radio leads, label content trails. Default-feeling for option lists."right": Radio trails, label content leads. Use when the label carries the primary visual weight.
When omitted in the card variant, the radio input is visually hidden and the entire card surface communicates the checked state on its own.
Must be one of "left", or "right".
- Global attributes are accepted. Additional HTML attributes forwarded to every radio
<input>in the group. Theformattribute is also forwarded to the hidden empty-value input so the group stays associated with the correct form when rendered outside of it. Supports all globals plus:["form"].
Slots
radio(required) - One entry per option in the group. The slot's inner block, when present, is rendered inside the card surface in place of the default label structure (only meaningful withvariant="card").Accepts attributes:value(:any) (required) - Value submitted when this option is selected, and used to compute initial state.label(:string) - Primary label rendered next to (or inside) the option.sublabel(:string) - Short inline qualifier rendered beside the option's label.description(:string) - Longer explanatory copy rendered beneath the option's label.disabled(:boolean) - Disables this option independently of the group-leveldisabledflag.class(:any) - Extra classes applied to the option. In the standard variant they are merged onto the radio<input>; in the card variant they are merged onto the card<label>, letting you restyle the surface (background, radius, sizing).checked(:boolean) - Forces this option to render selected on initial render regardless of the group'svalue. Use to seed a default selection when novalueis in scope yet.