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
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
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.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/radio.js";
import "@eunomia/elements/radio-list.js";
</script>
<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>
npm install @eunomia/elements
import "@eunomia/elements/radio.js";
import "@eunomia/elements/radio-list.js";
function PlanField() {
return (
<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>
);
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/radio.js";
import "@eunomia/elements/radio-list.js";
export function PlanField() {
return (
<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>
);
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/radio.js";
import "@eunomia/elements/radio-list.js";
</script>
<template>
<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>
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/radio.js";
import "@eunomia/elements/radio-list.js";
@Component({
selector: "app-plan-field",
template: `
<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>
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class PlanFieldComponent {}
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>
@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
eun-checkboxeun-selectGuidance
- Always group
eun-radioinside aeun-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-radiofor 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
hintin 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.
<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
| Name | Type | Default | Description |
|---|---|---|---|
| default-checked | boolean | — | The checked state applied when the radio connects, and restored on form reset |
| hide-error | boolean | false | Whether the error message is hidden |
| default-value | string | — | The default value applied when the radio connects, and restored on form reset |
| label | string | — | The label to display when not using the default slot |
Import the exact TypeScript type behind any property above, see
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| checked | boolean | false | Whether the radio is checked |
| defaultChecked | boolean | — | The checked state applied when the radio connects, and restored on form reset |
| disabled | boolean | false | Whether the radio is disabled |
| readonly | boolean | false | Whether the radio is read only |
| hideError | boolean | false | Whether the error message is hidden |
| required | boolean | false | Whether a selection is required |
| name | string | — | The name of the radio field |
| value | string | — | The value submitted with the radio |
| hint | string | — | A short message displayed under the radio |
| validators | Array<Validators<boolean>> | — | A list of validation rules applied to the checked state |
Slots
| Name | Description |
|---|---|
| (default) | The radio label |
Events
| Name | Type | Description |
|---|---|---|
| eunchange | ChangeEvent | Fired when the radio's checked state changes |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --radio-background | Sets the background color while not checked |
| --radio-background-hover | Sets the background color on hover |
| --radio-background-pressed | Sets the background color while pressed |
| --radio-background-checked | Sets the background color while checked |
| --radio-border-color | Sets the border color |
| --radio-border-color-hover | Sets the border color on hover |
| --radio-border-color-pressed | Sets the border color while pressed |
| --radio-border-color-checked | Sets the border color while checked |
| --radio-focus-outline-color | Sets the color of the focus outline used for keyboard navigation |
| --radio-error-color | Sets the color of the error message |
| --radio-hint-color | Sets the color of the hint message |
| --radio-size | Sets 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
| Name | Type | Default | Description |
|---|---|---|---|
| default-value | string | — | The value reselected when the list connects, and restored on form reset |
| hide-error | boolean | false | Whether the errors are hidden |
| inline | boolean | false | Whether the list's elements are displayed in a row |
Import the exact TypeScript type behind any property above, see
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| fields | EunomiaRadio[] | — | Getter allowing to recover the direct `eun-radio` (or `eun-radio-card`) children of the list. |
| active | EunomiaRadio | undefined | — | Getter allowing to recover the currently checked child, if any. |
| value | string | undefined | — | Setter allowing to check the child whose value matches, unchecking every other. |
| label | string | — | The title label of the list |
| instructions | string | — | Instructions for the user about the list selection |
| name | string | — | The field name, propagated to every child radio |
| disabled | boolean | false | Whether the list is disabled |
| readonly | boolean | false | Whether the list is read only |
| required | boolean | false | Whether a selection is required |
| hideError | boolean | false | Whether the errors are hidden |
| validators | Array<Validators<string | undefined>> | — | A list of validation rules applied to the group's selected value |
| description | — | Gets the current description or validation errors. Returns undefined if errors are hidden. | |
| form | — | Reference to the native form element this control belongs to. | |
| type | — | Returns the local tag name as the type of the field. |
Slots
| Name | Description |
|---|---|
| (default) | The list content. Only direct radio or radio card children participate |
| label | The title label of the list |
| instructions | Instructions for the user about the list selection |
Events
| Name | Type | Description |
|---|---|---|
| eunchange | ChangeEvent | Fired whenever the group's selection changes |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --radio-list-gap | Sets the gap between the labels and the list |
| --radio-list-options-gap | Sets the gap between the radios in the list |
| --radio-list-error-color | Sets 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
| Name | Type | Default | Description |
|---|---|---|---|
| default-checked | boolean | — | The checked state applied when the card connects, and restored on form reset |
| hide-error | boolean | false | Whether the error message is hidden |
| default-value | string | — | The default value applied when the card connects, and restored on form reset |
| icon | EunomiaIconName | — | An optional leading icon rendered inside the card |
| label | string | — | The label to display when not using the default slot |
Import the exact TypeScript type behind any property above, see
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| checked | boolean | false | Whether the card is checked |
| defaultChecked | boolean | — | The checked state applied when the card connects, and restored on form reset |
| disabled | boolean | false | Whether the card is disabled |
| readonly | boolean | false | Whether the card is read only |
| hideError | boolean | false | Whether the error message is hidden |
| required | boolean | false | Whether a selection is required |
| name | string | — | The name of the field |
| value | string | — | The value submitted with the card |
| hint | string | — | A short message displayed under the card |
| validators | Array<Validators<boolean>> | — | A list of validation rules applied to the checked state |
Slots
| Name | Description |
|---|---|
| (default) | The card label |
| icon | Replaces the leading icon entirely with any content, not just an icon element. Falls back to icon when empty |
Events
| Name | Type | Description |
|---|---|---|
| eunchange | ChangeEvent | Fired when the card's checked state changes |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --radio-card-background | Sets the background color while not checked |
| --radio-card-background-hover | Sets the background color on hover |
| --radio-card-background-pressed | Sets the background color while pressed |
| --radio-card-background-checked | Sets the background color while checked |
| --radio-card-background-error | Sets the background color while checked and invalid, falling back to radio-card-error-color when unset |
| --radio-card-border-color | Sets the border color while not checked |
| --radio-card-border-color-hover | Sets the border color on hover |
| --radio-card-border-color-checked | Sets the border color while checked |
| --radio-card-border-width | Sets the width of the border |
| --radio-card-border-radius | Sets the corner radius of the card |
| --radio-card-padding | Sets the padding inside the card |
| --radio-card-min-width | Sets the minimum width of the card |
| --radio-card-icon-color | Sets the color of the leading icon while not checked |
| --radio-card-icon-color-checked | Sets the color of the leading icon while checked |
| --radio-card-error-color | Sets the color of the error message |
| --radio-card-hint-color | Sets the color of the hint message |
| --radio-card-focus-outline-color | Sets the color of the focus outline used for keyboard navigation |
| --radio-background | Sets the background color while not checked |
| --radio-background-hover | Sets the background color on hover |
| --radio-background-pressed | Sets the background color while pressed |
| --radio-background-checked | Sets the background color while checked |
| --radio-border-color | Sets the border color |
| --radio-border-color-hover | Sets the border color on hover |
| --radio-border-color-pressed | Sets the border color while pressed |
| --radio-border-color-checked | Sets the border color while checked |
| --radio-focus-outline-color | Sets the color of the focus outline used for keyboard navigation |
| --radio-error-color | Sets the color of the error message |
| --radio-hint-color | Sets the color of the hint message |
| --radio-size | Sets the size of the radio |
Basic
<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():
<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.
<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.
<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
<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.
<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
<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
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.
<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
<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
<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.
<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.
<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.
<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 hostaria-checked="true"\|"false": reflectschecked. Unlikeeun-checkbox, there is no"mixed"value, since a radio has no indeterminate statearia-disabled/aria-readonly: kept in sync withdisabled/readonlyaria-invalid: kept in sync withvalid, oncedisabledorhideErrorare also accounted foraria-description: set fromhint/error content while present (via the inheriteddescription), removed otherwisearia-label: set from thelabelproperty 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
labelproperty:eun-radiosetsaria-labelon the host directly from it, an explicit, guaranteed accessible name. - Slot text content instead (
<eun-radio>Pro</eun-radio>, used throughout this page). No explicitaria-labelis 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
iconis purely decorative (eun-iconrenders its SVG witharia-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