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
| 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-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. The hint stays visible |
| 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 errors are hidden. The hint stays visible |
| invalid | boolean | false | Shows the field as invalid without any message, for an error displayed elsewhere |
| error | string | — | An error computed outside the field, by the form or by the server. While set, the field is invalid and shows this message right away |
| 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 |
| --field-label-font-size | Sets the font size of the label |
| --field-message-font-size | Sets the font size of the hint, error messages and instructions |
Into a list
eun-radio-list groups eun-radio children into a single "select 1 of M"
field: a labeled 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. The hint stays visible |
| 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 |
| hideLabel | boolean | false | Whether the label is visually hidden. It still gives the field its accessible name, and the instructions stay visible |
| 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. The hint stays visible |
| invalid | boolean | false | Shows the field as invalid without any message, for an error displayed elsewhere |
| error | string | — | An error computed outside the field, by the form or by the server. While set, the field is invalid and shows this message right away |
| validators | Array<Validators<string | undefined>> | — | A list of validation rules applied to the group's selected value |
| errorsHidden | — | Whether the error messages are hidden, by hideError or by an ancestor `<fieldset hide-error>`. | |
| showError | — | Whether the field currently shows its error state (styling and `aria-invalid`): not disabled, and either forced by invalid or showing its error messages. | |
| showErrorMessages | — | Whether the field currently shows its error messages : invalid, not disabled, not errorsHidden, and either left once (`touched`), changed while errorOnChange is set (`dirty`), checked by its form, or given an external `error`. Typing alone never reveals an error, so an unfinished entry isn't reported as invalid; once shown, the error follows the value live. | |
| pristine | — | Whether the value hasn't changed since the field connected (or since the last reset). The inverse of dirty. | |
| untouched | — | Whether the field hasn't lost focus yet since it connected (or since the last reset). The inverse of touched. | |
| description | — | Gets the validation errors while showErrorMessages is true, the hint otherwise. | |
| form | — | Reference to the native form element this control belongs to. | |
| type | — | Returns the local tag name as the type of the field. | |
| dirty | boolean | false | Whether the value has changed since the field connected (or since the last reset). Derived from `eunchange`, the event every field's own user-interaction handler already dispatches (and only that: setting `.value`/`.defaultValue` from outside, as a controlled React field does on every render, never dispatches it), so a controlled field stays pristine across ordinary re-renders and only turns dirty from genuine user input. |
| touched | boolean | false | Whether the field has lost focus at least once since it connected (or since the last reset). |
| validationRequested | boolean | false | Whether the form asked for this field's validity, on a submit attempt or a `reportValidity()` call, since it connected (or since the last reset). Reveals errors on a field the user never reached. |
| controlled | boolean | false | Opts this field out of the native reset algorithm's value wipe: when
`true`, `formResetCallback` leaves `value`/`checked` untouched.
Exists for consumers who drive |
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 |
| --field-label-font-size | Sets the font size of the label |
| --field-message-font-size | Sets the font size of the hint, error messages and instructions |
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. The hint stays visible |
| 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 errors are hidden. The hint stays visible |
| invalid | boolean | false | Shows the field as invalid without any message, for an error displayed elsewhere |
| error | string | — | An error computed outside the field, by the form or by the server. While set, the field is invalid and shows this message right away |
| 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-padding-block | Sets the vertical padding, as one length applied to both sides. Ignored when --radio-card-padding is set |
| --radio-card-padding-inline | Sets the horizontal padding, as one length applied to both sides. Ignored when --radio-card-padding is set |
| --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 |
| --field-label-font-size | Sets the font size of the label |
| --field-message-font-size | Sets the font size of the hint, error messages and instructions |
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).
Grouped list
eun-radio-list wraps a set of eun-radio children in a radiogroup
named by its label, 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;
}
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;
}
The custom properties that change colors, borders, shadows and corners, at a glance. The API tab lists every property of the component.
| Variable | Sets |
|---|---|
--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-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-list-error-color |
Sets the color of the list's error state |
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 enabled 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.
Semantics and screen readers
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 for, and forced totruewhileinvalidis setaria-description: the hint, or the error once it shows, kept in sync as it changes and removed when there's none. The message rendered below is hidden from assistive technology, so it isn't read again as part of the control's namearia-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), named from the list's label through aria-label, with
aria-describedby pointing at its instructions and its hint or error,
aria-required mirroring the list's own required, aria-readonly
mirroring readonly, and aria-invalid mirroring the group's error 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. The DOM focus itself moves from radio to radio as the arrow
keys rove, so assistive technology announces each one as it's reached.
Label accessibility
role="radio" needs an accessible name, which ARIA lets come either from
the author or from the element's content. 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 is computed from the flattened shadow-tree content: the slotted text. The hint and error rendered below are kept out of that computation (seearia-descriptionabove).
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.
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
Field states
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.
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.8 Target Size (Minimum).
- 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