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

ComponentBest suited for
radio_group/1Single selection from a mutually exclusive set of options.
Fluxon.Components.CheckboxBoolean opt-in or multi-select from a known list.
Fluxon.Components.SelectSingle selection when the option list is long or space is tight.
Fluxon.Components.SwitchOn/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 (or field.value when bound) stringifies equal to the option's value. The comparison uses to_string/1 on both sides, so integer values match string values with the same representation (for example value={1} against <:radio value="1" />).
  • The slot's own checked attribute is true. Use it to seed a default selection when no value is 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.

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

radio_group(assigns)

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 :radio slot input derives its own id by appending its zero-based index to this value (a group with id="plan" produces "plan-0", "plan-1", and so on). When omitted, an ID is generated automatically; a field binding reuses field.id instead.

    Defaults to nil.

  • name (:string) - Form field name shared by every radio input in the group. Required when field is not provided.

  • value (:any) - Currently selected value. The option whose value stringifies equal to this attribute renders pre-selected. Provided automatically when field is set; defaults to nil (nothing selected) otherwise.

  • label (:string) - Heading rendered above the radio buttons via Fluxon.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 main label (for example "Optional", "Recommended"). Has no effect when label is nil.

    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 when field is 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 derives id, name, value, and errors from the field and runs error messages through the configured translator.

  • disabled (:boolean) - Disables every radio input in the group when true. Individual options can still be disabled via the :radio slot's own disabled attribute 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 when variant="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. The form attribute 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 with variant="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-level disabled flag.

    • 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's value. Use to seed a default selection when no value is in scope yet.