Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Radio

Radio represents a single choice, such as picking one plan or one shipping method, always as part of a group sharing a name. It's not a wrapper around a native input. The whole element behaves as the control itself, so its accessible name, checked state, and validation all come from its own properties. Grouped with others in a list, only one option can ever be checked at a time, and the list adds the arrow-key navigation a group of options needs to stay easy to use with a keyboard (see the Accessibility tab).

Dependencies

eun-icon · for the card variant's leading icon eun-label · for the list's group label, if it has one
Overview API Examples Accessibility
Free Pro Enterprise

When to use

Use radio when someone must pick exactly one option out of a short, fully-visible list, such as a plan, a shipping method, or a yes, no, or maybe. If more than one can be true at once, that's checkbox territory instead. Once the list grows past five or six options, a select dropdown scales better than a wall of radio buttons.

Install & usage

Pick a framework in the toolbar above and these snippets adapt.

npm install @eunomia/elements
import "@eunomia/elements/radio.js";
import "@eunomia/elements/radio-list.js";
<eun-radio-list name="plan" label="Choose a plan">
  <eun-radio value="free">Free</eun-radio>
  <eun-radio value="pro">Pro</eun-radio>
  <eun-radio value="enterprise">Enterprise</eun-radio>
</eun-radio-list>

Importing the files registers <eun-radio> and <eun-radio-list> as custom elements, with no further setup needed. It works with any framework, or none, since these are standard web components.

Grouping several radios under one name? Import @eunomia/elements/radio-list.js too. Its properties are in the API tab, right below eun-radio's own, and the "Grouped list" section of the Examples tab shows it customized. eun-radio-list doesn't have a page of its own since it's really just eun-radio's own group behavior, not a separate component to learn.

import "@eunomia/elements/radio.js";
import "@eunomia/elements/radio-list.js";
<eun-radio-list name="plan" label="Choose a plan">
  <eun-radio value="free">Free</eun-radio>
  <eun-radio value="pro">Pro</eun-radio>
</eun-radio-list>

Reaching for the card variant? Import @eunomia/elements/radio-card.js instead of (or alongside) radio.js. It's a separate entry point, not bundled into eun-radio by default. Since eun-radio-card extends eun-radio, importing radio-card.js alone already registers both elements, so a separate radio.js import isn't required just to use the card variant.

import "@eunomia/elements/radio-card.js";
<eun-radio-card value="pro" icon="star">Pro</eun-radio-card>
Using a bundler (Vite, webpack, Rollup, …) or an import map resolves @eunomia/elements/radio.js to one shared module no matter how many files import it, so this doesn't apply there. It only matters if you self-host separately pre-bundled files for each component (the way this documentation site itself loads its live previews) and reference both with plain <script type="module" src="…"> tags. Load radio-card.js before radio.js. Whichever loads first "wins" the shared eun-radio custom-element registration, and the other logs a harmless "already been used" console error and stops there, but if radio.js loads first, that error happens inside radio-card.js's own bundle (which also needs to define eun-radio to extend it), aborting the rest of that script before it reaches its own eun-radio-card registration, leaving <eun-radio-card> undefined and rendered as plain, unstyled text.
<!-- Correct order for plain <script> tags, no bundler: -->
<script type="module" src="radio-card.js"></script>
<script type="module" src="radio.js"></script>

Alternatives

You want .. Prefers More than one option to be true at once eun-checkbox A long list of options, past ~5-6 eun-select

Guidance

  • Always group eun-radio inside a eun-radio-list: a lone radio outside a group has nothing to be "one of"
  • Always set the list's label (or slot one): see the Accessibility tab
  • Using a single, standalone eun-radio for a binary on/off choice: see Alternatives above
  • Using a radio list for more than ~5-6 options: a select dropdown would scale better
  • Pre-checking nothing on a required choice the user must actively make: bias toward the safest, most common option instead, or accept the validation error until they choose
  • Hiding the hint in a tooltip instead of rendering it under the field
  • Overriding colors with inline styles instead of the --radio-* CSS variables

Card variant

eun-radio-card is a card-styled eun-radio, with the exact same API (checked, disabled, readonly, required, name, value, label, hint, validators, every eunchange/keyboard/form behavior) plus an optional leading icon, for a single choice presented as a bigger, more visual tile (a plan, a shipping method) instead of a plain inline dot. It turns primary-colored on click and when checked, the same signal eun-radio's own fill communicates, just scaled up to the whole card.

Free Pro Enterprise
<eun-radio-list name="plan" label="Choose a plan" inline>
  <eun-radio-card value="free" icon="bolt">Free</eun-radio-card>
  <eun-radio-card value="pro" icon="star">Pro</eun-radio-card>
  <eun-radio-card value="enterprise" icon="account_balance">
    Enterprise
  </eun-radio-card>
</eun-radio-list>

Grouped under one name inside a eun-radio-list exactly like plain eun-radio (it recognizes eun-radio-card children too, including the roving-tabindex/arrow-key navigation a group of options needs, see the Accessibility tab), with inline laying them out in a row instead of the list's default vertical stack. See the "Card variant" sections in the API and Examples tabs below, including a standalone, ungrouped card.

Live testing

Properties

Radio <eun-radio>

Attributes

NameTypeDefaultDescription
default-checkedbooleanThe checked state applied when the radio connects, and restored on form reset
hide-errorbooleanfalseWhether the error message is hidden
default-valuestringThe default value applied when the radio connects, and restored on form reset
labelstringThe label to display when not using the default slot

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
checkedbooleanfalseWhether the radio is checked
defaultCheckedbooleanThe checked state applied when the radio connects, and restored on form reset
disabledbooleanfalseWhether the radio is disabled
readonlybooleanfalseWhether the radio is read only
hideErrorbooleanfalseWhether the error message is hidden
requiredbooleanfalseWhether a selection is required
namestringThe name of the radio field
valuestringThe value submitted with the radio
hintstringA short message displayed under the radio
validatorsArray<Validators<boolean>>A list of validation rules applied to the checked state

Slots

NameDescription
(default)The radio label

Events

NameTypeDescription
eunchangeChangeEventFired when the radio's checked state changes

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--radio-backgroundSets the background color while not checked
--radio-background-hoverSets the background color on hover
--radio-background-pressedSets the background color while pressed
--radio-background-checkedSets the background color while checked
--radio-border-colorSets the border color
--radio-border-color-hoverSets the border color on hover
--radio-border-color-pressedSets the border color while pressed
--radio-border-color-checkedSets the border color while checked
--radio-focus-outline-colorSets the color of the focus outline used for keyboard navigation
--radio-error-colorSets the color of the error message
--radio-hint-colorSets the color of the hint message
--radio-sizeSets the size of the radio

Into a list

eun-radio-list groups eun-radio children into a single "select 1 of M" field: a fieldset/legend wrapper around a radiogroup that propagates name/disabled/readonly down to every child, applies a roving tabindex and arrow-key navigation between them, and exposes the group's selection as a plain value: string | undefined, with its own eunchange event and validators. It doesn't have a documentation page of its own : this is its whole API. See the Examples tab ("Grouped list") for customizing it: inline layout, group validation, and disabled/readonly.

Radio list <eun-radio-list>

Attributes

NameTypeDefaultDescription
default-valuestringThe value reselected when the list connects, and restored on form reset
hide-errorbooleanfalseWhether the errors are hidden
inlinebooleanfalseWhether the list's elements are displayed in a row

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
fieldsEunomiaRadio[]Getter allowing to recover the direct `eun-radio` (or `eun-radio-card`) children of the list.
activeEunomiaRadio | undefinedGetter allowing to recover the currently checked child, if any.
valuestring | undefinedSetter allowing to check the child whose value matches, unchecking every other.
labelstringThe title label of the list
instructionsstringInstructions for the user about the list selection
namestringThe field name, propagated to every child radio
disabledbooleanfalseWhether the list is disabled
readonlybooleanfalseWhether the list is read only
requiredbooleanfalseWhether a selection is required
hideErrorbooleanfalseWhether the errors are hidden
validatorsArray<Validators<string | undefined>>A list of validation rules applied to the group's selected value
descriptionGets the current description or validation errors. Returns undefined if errors are hidden.
formReference to the native form element this control belongs to.
typeReturns the local tag name as the type of the field.

Slots

NameDescription
(default)The list content. Only direct radio or radio card children participate
labelThe title label of the list
instructionsInstructions for the user about the list selection

Events

NameTypeDescription
eunchangeChangeEventFired whenever the group's selection changes

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--radio-list-gapSets the gap between the labels and the list
--radio-list-options-gapSets the gap between the radios in the list
--radio-list-error-colorSets the color of the list's error state

Card variant

eun-radio-card extends eun-radio directly, so every property above applies unchanged: only icon is new, and styles/render() are overridden for the card look. See the --radio-card-* custom properties below for the card-specific visual variables (--radio-* itself doesn't apply, since the card shares no CSS selectors with the plain dot/label markup).

Radio card <eun-radio-card>

Attributes

NameTypeDefaultDescription
default-checkedbooleanThe checked state applied when the card connects, and restored on form reset
hide-errorbooleanfalseWhether the error message is hidden
default-valuestringThe default value applied when the card connects, and restored on form reset
iconEunomiaIconNameAn optional leading icon rendered inside the card
labelstringThe label to display when not using the default slot

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
checkedbooleanfalseWhether the card is checked
defaultCheckedbooleanThe checked state applied when the card connects, and restored on form reset
disabledbooleanfalseWhether the card is disabled
readonlybooleanfalseWhether the card is read only
hideErrorbooleanfalseWhether the error message is hidden
requiredbooleanfalseWhether a selection is required
namestringThe name of the field
valuestringThe value submitted with the card
hintstringA short message displayed under the card
validatorsArray<Validators<boolean>>A list of validation rules applied to the checked state

Slots

NameDescription
(default)The card label
iconReplaces the leading icon entirely with any content, not just an icon element. Falls back to icon when empty

Events

NameTypeDescription
eunchangeChangeEventFired when the card's checked state changes

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--radio-card-backgroundSets the background color while not checked
--radio-card-background-hoverSets the background color on hover
--radio-card-background-pressedSets the background color while pressed
--radio-card-background-checkedSets the background color while checked
--radio-card-background-errorSets the background color while checked and invalid, falling back to radio-card-error-color when unset
--radio-card-border-colorSets the border color while not checked
--radio-card-border-color-hoverSets the border color on hover
--radio-card-border-color-checkedSets the border color while checked
--radio-card-border-widthSets the width of the border
--radio-card-border-radiusSets the corner radius of the card
--radio-card-paddingSets the padding inside the card
--radio-card-min-widthSets the minimum width of the card
--radio-card-icon-colorSets the color of the leading icon while not checked
--radio-card-icon-color-checkedSets the color of the leading icon while checked
--radio-card-error-colorSets the color of the error message
--radio-card-hint-colorSets the color of the hint message
--radio-card-focus-outline-colorSets the color of the focus outline used for keyboard navigation
--radio-backgroundSets the background color while not checked
--radio-background-hoverSets the background color on hover
--radio-background-pressedSets the background color while pressed
--radio-background-checkedSets the background color while checked
--radio-border-colorSets the border color
--radio-border-color-hoverSets the border color on hover
--radio-border-color-pressedSets the border color while pressed
--radio-border-color-checkedSets the border color while checked
--radio-focus-outline-colorSets the color of the focus outline used for keyboard navigation
--radio-error-colorSets the color of the error message
--radio-hint-colorSets the color of the hint message
--radio-sizeSets the size of the radio

Basic

Free Pro Enterprise
<eun-radio-list name="plan" label="Choose a plan">
  <eun-radio value="free">Free</eun-radio>
  <eun-radio value="pro">Pro</eun-radio>
  <eun-radio value="enterprise">Enterprise</eun-radio>
</eun-radio-list>

Read the selection back as a plain string, and listen for eunchange on the list rather than on every child individually:

const list = document.querySelector("eun-radio-list");
list.addEventListener("eunchange", () => console.log(list.value)); // e.g. "pro"

Default selection

Either check a child directly in markup, or set the list's own default-value, since both are restored on a native form reset():

Free Pro Enterprise
<eun-radio-list name="plan" label="Choose a plan" default-value="pro">
  <eun-radio value="free">Free</eun-radio>
  <eun-radio value="pro">Pro</eun-radio>
  <eun-radio value="enterprise">Enterprise</eun-radio>
</eun-radio-list>

States

Disabled

Disabled radios remain visible but can't be selected, and are excluded from the tab order and from form submission.

Free Pro Enterprise
<eun-radio-list name="plan" label="Choose a plan" disabled>
  <eun-radio value="free">Free</eun-radio>
  <eun-radio value="pro" checked>Pro</eun-radio>
  <eun-radio value="enterprise">Enterprise</eun-radio>
</eun-radio-list>

Readonly

A readonly list looks active, stays focusable, and its value still submits, but selection can't be changed. Unlike disabled, it still communicates its current state instead of looking inert.

Free Pro Enterprise
<eun-radio-list name="plan" label="Choose a plan" readonly>
  <eun-radio value="free">Free</eun-radio>
  <eun-radio value="pro" checked>Pro</eun-radio>
  <eun-radio value="enterprise">Enterprise</eun-radio>
</eun-radio-list>

Error

Free Pro Enterprise
<eun-radio-list
  name="plan"
  label="Choose a plan"
  required
  hint="Select a plan to continue"
>
  <eun-radio value="free">Free</eun-radio>
  <eun-radio value="pro">Pro</eun-radio>
  <eun-radio value="enterprise">Enterprise</eun-radio>
</eun-radio-list>

required on the list means "at least one option checked": its value getter reflects the current selection (undefined until one is checked), so the built-in check already works with no extra wiring. Layer validators on top for anything more specific (see "Group validation" below).

Custom radio

Override the --radio-* CSS variables, listed in full in the API tab.

Custom radio
<eun-radio class="custom-radio" checked>Custom radio</eun-radio>
.custom-radio {
  --radio-border-color-checked: #be185d;
  --radio-background-checked: #be185d;
}

Grouped list

eun-radio-list wraps a set of eun-radio children in a fieldset / legend around a radiogroup so the group reads as one unit to assistive technology. Each child radio still submits its own value under the shared name independently when checked, exactly like a native group of <input type="radio"> sharing a name, so this component never duplicates what its children already do on their own, only adding grouping semantics, roving-tabindex/arrow-key navigation, and a convenience API on top. Its properties are documented in the API tab, right below eun-radio's own: the examples below are about customizing it.

Inline layout

Free Pro Enterprise
<eun-radio-list name="plan" label="Choose a plan" inline>
  <eun-radio value="free">Free</eun-radio>
  <eun-radio value="pro">Pro</eun-radio>
  <eun-radio value="enterprise">Enterprise</eun-radio>
</eun-radio-list>

Group validation

Free Pro Enterprise
const list = document.querySelector("eun-radio-list");
list.validators = [
  {
    isValid: (value?: string) => value === "enterprise",
    message: "Enterprise is required for this demo",
  },
];
list.checkValidity(list.value);

The list propagates its own valid down to every child, so all of them render invalid together while the selection doesn't satisfy the rule.

Disabled and readonly

Setting disabled or readonly on the list propagates it to every child radio, no need to repeat it on each one.

Free Pro
<eun-radio-list name="plan" label="Choose a plan" disabled>
  <eun-radio value="free" checked>Free</eun-radio>
  <eun-radio value="pro">Pro</eun-radio>
</eun-radio-list>

Card display

Basic, with icons

Free Pro Enterprise
<eun-radio-list name="plan" label="Choose a plan" inline>
  <eun-radio-card value="free" icon="bolt">Free</eun-radio-card>
  <eun-radio-card value="pro" icon="star">Pro</eun-radio-card>
  <eun-radio-card value="enterprise" icon="account_balance">
    Enterprise
  </eun-radio-card>
</eun-radio-list>

Grouped in a eun-radio-list, eun-radio-card gets the exact same roving-tabindex/arrow-key radiogroup navigation as plain eun-radio (see the Accessibility tab): Tab reaches the group once, ArrowDown/ArrowRight and ArrowUp/ArrowLeft move the selection between cards.

Without an icon

Yes, sign me up
<eun-radio-card name="signup" value="yes">Yes, sign me up</eun-radio-card>

Custom icon

Need something other than a eun-icon, or a different icon library entirely? Slot your own content into icon instead. It replaces the prop-driven icon entirely.

Pro
<eun-radio-card value="pro">
  <svg
    slot="icon"
    width="24"
    height="24"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
  >
    <path
      d="m12 2 2.9 6.9L22 10l-5.5 4.8L18 22l-6-3.7L6 22l1.5-7.2L2 10l7.1-1.1L12 2Z"
    ></path>
  </svg>
  Pro
</eun-radio-card>

Disabled, readonly, and error

Three independent fields side by side for comparison, not a group: each keeps its own state directly, so they're plain siblings here rather than wrapped in a eun-radio-list, since the list always propagates its own disabled/readonly down to every child on connect, which would overwrite each card's individually-set state with the list's shared one.

Disabled Readonly Required
<eun-radio-card value="free" icon="bolt" disabled>Disabled</eun-radio-card>
<eun-radio-card value="pro" icon="star" readonly checked
  >Readonly</eun-radio-card
>
<eun-radio-card
  value="enterprise"
  icon="account_balance"
  required
  hint="Required"
  >Required</eun-radio-card
>

Custom card

Override the --radio-card-* CSS variables, listed in full in the API tab.

Custom card
<eun-radio-card class="custom-radio-card" icon="star" checked>
  Custom card
</eun-radio-card>
.custom-radio-card {
  --radio-card-border-color-checked: #be185d;
  --radio-card-background-checked: #fdf2f8;
  --radio-card-icon-color-checked: #be185d;
}

This audit covers eun-radio grouped in eun-radio-list, its only intended usage (see the Overview tab's Guidance). A lone, ungrouped eun-radio is also covered where its behavior differs.

Keyboard interactions

Key Action
Tab / Shift+Tab Moves focus in/out of the group, a single tab stop for the whole eun-radio-list
Space Checks the focused radio, or the first enabled one if none is checked yet
ArrowDown / ArrowRight Moves the check (and focus) to the next enabled radio, wrapping from the last to the first
ArrowUp / ArrowLeft Moves the check (and focus) to the previous enabled radio, wrapping from the first to the last

This is the standard native radiogroup behavior. Unlike eun-checkbox-list (where every child keeps its own place in the tab order), a eun-radio-list is a single tab stop: eun-radio-list applies a roving tabindex, so only the checked radio (or the first one, if none is checked yet) is ever reachable via Tab. Arrow keys move within the group without leaving it, immediately checking whatever they land on, since there is no separate "moved focus but not yet selected" state, matching how native <input type="radio"> groups behave.

A lone, ungrouped eun-radio only supports Space, which checks it, since arrow-key roving is eun-radio-list's job and a solitary radio has no sibling to rove to.

Aria attributes

eun-radio doesn't wrap a native <input>. The host element itself is the control, so every ARIA state lives directly on it:

  • role="radio", set on connect if not already present on the host
  • aria-checked="true"\|"false": reflects checked. Unlike eun-checkbox, there is no "mixed" value, since a radio has no indeterminate state
  • aria-disabled / aria-readonly: kept in sync with disabled / readonly
  • aria-invalid: kept in sync with valid, once disabled or hideError are also accounted for
  • aria-description: set from hint/error content while present (via the inherited description), removed otherwise
  • aria-label: set from the label property when it's used (see Label accessibility below)

eun-radio-list wraps its children in role="radiogroup" (on the options container) inside a real <fieldset>/<legend>, with aria-labelledby pointing at the legend, aria-describedby at its error/hint message, aria-required mirroring the list's own required, aria-readonly mirroring readonly, and aria-invalid mirroring the group's valid state: all of them supported states for the radiogroup role, unlike eun-checkbox-list's plain role="group", which doesn't carry the same guarantees. eun-radio-list also maintains aria-activedescendant on that same container, pointing at the currently-checked (or first) child's id, which is what lets assistive technology announce the active option as focus roves between radios without the DOM focus itself ever actually leaving the container between arrow presses.

Label accessibility

role="radio" requires an author-supplied accessible name. Unlike role="button", for example, ARIA does not allow deriving one from content for this role. Two ways to provide it, both supported:

  • Set the label property: eun-radio sets aria-label on the host directly from it, an explicit, guaranteed accessible name.
  • Slot text content instead (<eun-radio>Pro</eun-radio>, used throughout this page). No explicit aria-label is set in that case, so the name comes from the flattened shadow-tree content instead: the slotted text is what ends up read as the accessible name in every browser tested, but this relies on content-based name computation rather than an explicit author-supplied one.

Always provide one or the other. An unlabelled radio is exactly as disruptive for screen-reader users as an unlabelled text field.

Always set the list's own label (or slot one) too, since it names the radiogroup itself, distinct from each radio's own label naming its individual option. Without it, a screen reader announcing "radio group" with no further context leaves the user unable to tell what the choice is even about.

Disabled vs. readonly

disabled removes a radio from the group's tab order and excludes its value from form submission entirely. readonly (set on the list, propagated to every child) keeps the group focusable and its value submitted, and additionally prevents changing the selection, communicating "you can't change this right now", not "this doesn't apply".

A disabled control may cause usability and accessibility issues for people with disabilities relying on that state to still be inspectable. Prefer readonly on eun-radio-list whenever the current selection still needs to stay perceivable and focusable.

Required field

required means "must be checked", validated against the radio's actual checked state, not against its value attribute, which is typically a fixed string (e.g. value="pro") present whether or not the radio is checked, and would otherwise never look "empty" to a generic required check. On a eun-radio-list, required means "one option checked" (the group's value is undefined until then), validated against the group's real selection, which is also reflected as aria-required="true" on the radiogroup container itself.

Add a visible note near the top of the form in addition to relying on the error message alone, and prefer hint (or a group-level validators message on eun-radio-list) to explain why a selection is required, not just that it is. Avoid leaving a required choice unchecked by default when a safe, common default exists, since forcing an otherwise-avoidable validation error is worse than pre-selecting the most common option.

Screen reader restitution

By default, screen readers read out the group's accessible name and instructions on entry, then each radio's own name, checked state, and position in the group (e.g. "Pro, radio button, 2 of 3") as the user moves between them with arrow keys. The exact phrasing and order may vary depending on the screen reader and its configuration. The disabled state is rendered differently depending on the screen reader:

  • VoiceOver (macOS/iOS): grayed out
  • NVDA and JAWS: unavailable
  • Narrator and TalkBack: disabled

Card variant (eun-radio-card)

eun-radio-card extends eun-radio directly rather than reimplementing it, so every ARIA attribute, keyboard interaction, and label requirement documented above carries over completely unchanged, including grouped behavior: eun-radio-list recognizes eun-radio-card children exactly like plain eun-radio (same role="radiogroup" wrapper, same roving tabindex, same arrow-key navigation). A lone, ungrouped eun-radio-card behaves the same as a lone, ungrouped eun-radio: only Space toggles it, since there's no sibling to rove to. Two differences worth calling out:

  • The entire card is the click target, not just the small checked indicator in its corner, a strictly larger hit area than a plain radio's own dot, which helps meet WCAG 2.5.5 Target Size.
  • The optional leading icon is purely decorative (eun-icon renders its SVG with aria-hidden="true", see the Icon component's own documentation). It never carries meaning on its own, only reinforcing the text label already present. Never rely on the icon alone to convey what the option is.

Reference links

WAI-ARIA Authoring Practices: Radio Group Pattern
WAI Web Accessibility Tutorials: Labeling Controls
WAI Web Accessibility Tutorials: Grouping Controls