role group
Radio group draft v0.1.0
Lets people choose exactly one option from a short, visible set.
A radio group shows every option at once, with a legend naming the choice, and lets people check exactly one of them: checking an option unchecks the previous one. Use it when the options are few and worth reading side by side; a long list of options is a Select, and independent on/off options are Checkboxes. The group is one component: the options are its repeated part, each with its own label, description, value and disabled state.
Specification
From packages/components/specs/components/radio-group.spec.json
Raw file packages/components/specs/components/radio-group.spec.json
{
"$schema": "../schema/component.schema.json",
"id": "radio-group",
"name": "Radio group",
"version": "0.1.0",
"status": "draft",
"role": "group",
"summary": "Lets people choose exactly one option from a short, visible set.",
"description": "A radio group shows every option at once, with a legend naming the choice, and lets people check exactly one of them: checking an option unchecks the previous one. Use it when the options are few and worth reading side by side; a long list of options is a Select, and independent on/off options are Checkboxes. The group is one component: the options are its repeated part, each with its own label, description, value and disabled state.",
"tags": [
"form",
"input",
"selection"
],
"anatomy": [
{
"id": "container",
"role": "container",
"required": true,
"description": "Root: the group of options (a fieldset on the web). Carries the orientation, the size and the states forced for review."
},
{
"id": "legend",
"role": "label",
"required": true,
"description": "Text naming the choice. Provides the group's accessible name."
},
{
"id": "description",
"role": "description",
"required": false,
"description": "Optional help text under the legend, associated with every option as its description."
},
{
"id": "option",
"role": "container",
"required": true,
"repeatable": true,
"description": "One option: its radio, label and description on a grid. Takes the states of the control it holds.",
"properties": [
{
"id": "label",
"name": "Label",
"type": "string",
"required": true,
"bindsTo": "option-label",
"description": "Name of the option. Provides the radio's accessible name."
},
{
"id": "value",
"name": "Value",
"type": "string",
"description": "Value submitted when the option is checked. Defaults to the option's position."
},
{
"id": "description",
"name": "Description",
"type": "string",
"bindsTo": "option-description",
"description": "Help text under the option's label, exposed as the radio's description."
},
{
"id": "checked",
"name": "Checked",
"type": "boolean",
"default": false,
"description": "Whether this option is the chosen one. At most one option is checked."
},
{
"id": "disabled",
"name": "Disabled",
"type": "boolean",
"default": false,
"description": "The option stays visible, dimmed, and cannot be chosen."
}
]
},
{
"id": "control",
"role": "indicator",
"required": true,
"parent": "option",
"description": "The radio: the native radio button, drawn by the component. Receives focus, holds the checked state and shows it with a dot."
},
{
"id": "option-label",
"role": "label",
"required": true,
"parent": "option",
"description": "Text naming the option; activating it checks the radio."
},
{
"id": "option-description",
"role": "description",
"required": false,
"parent": "option",
"description": "Optional help text under the option's label."
},
{
"id": "error-message",
"role": "errormessage",
"required": false,
"visibleIn": [
"invalid"
],
"description": "Text describing the error when the group is invalid (no option chosen in a required group). Associated with every option as a description."
}
],
"properties": [
{
"id": "orientation",
"name": "Orientation",
"type": "enum",
"default": "vertical",
"description": "How the options flow.",
"affects": [
"container"
],
"options": [
{
"value": "vertical",
"description": "One option per line, the default: easiest to scan and leaves room for descriptions."
},
{
"value": "horizontal",
"description": "Options on one line, wrapping when needed: two or three short options (Yes / No)."
}
]
},
{
"id": "size",
"name": "Size",
"type": "enum",
"default": "medium",
"description": "Radio size, option label and description text size scale together.",
"affects": [
"option",
"control",
"option-label",
"option-description"
],
"options": [
{
"value": "small",
"description": "Dense lists and tables."
},
{
"value": "medium",
"description": "Default."
}
]
},
{
"id": "label",
"name": "Legend",
"type": "string",
"required": true,
"bindsTo": "legend",
"description": "The question or the thing being chosen (Delivery method)."
},
{
"id": "description",
"name": "Description",
"type": "string",
"bindsTo": "description",
"description": "Help text for the whole choice."
},
{
"id": "name",
"name": "Name",
"type": "string",
"description": "Form field name shared by every option, which is what makes them one group. Defaults to the group's id."
},
{
"id": "options",
"name": "Options",
"type": "list",
"required": true,
"of": [
"option"
],
"bindsTo": "option",
"description": "The options, in order."
},
{
"id": "errorMessage",
"name": "Error message",
"type": "string",
"bindsTo": "error-message",
"description": "What is wrong and what to do, shown when the group is invalid."
},
{
"id": "required",
"name": "Required",
"type": "boolean",
"default": false,
"description": "An option must be chosen to submit the form."
},
{
"id": "invalid",
"name": "Invalid",
"type": "boolean",
"default": false,
"description": "Validation failed: every radio takes the error border and the error message is shown."
},
{
"id": "disabled",
"name": "Disabled",
"type": "boolean",
"default": false,
"description": "Disables the whole group; the chosen option stays visible."
}
],
"states": [
{
"id": "default",
"trigger": "interaction",
"parts": [
"option"
],
"description": "Resting, unchecked: an outlined empty circle."
},
{
"id": "hover",
"trigger": "interaction",
"inputs": [
"pointer"
],
"parts": [
"option"
],
"description": "Pointer over the radio or its label. The empty circle takes a light tint; a checked radio does not change. Needs a pointing device: touch has no hover."
},
{
"id": "focus-visible",
"trigger": "interaction",
"inputs": [
"keyboard"
],
"parts": [
"option"
],
"description": "Keyboard focus on a radio. Shows the shared focus ring outside it; pointer focus shows none."
},
{
"id": "checked",
"trigger": "property",
"property": "options",
"parts": [
"option"
],
"description": "The chosen option (the entry's checked property): the circle fills with the brand color and shows a dot."
},
{
"id": "invalid",
"trigger": "property",
"property": "invalid",
"parts": [
"option"
],
"description": "Validation failed on the group: every radio border takes the error color and the error message is shown."
},
{
"id": "disabled",
"trigger": "property",
"property": "disabled",
"parts": [
"option"
],
"description": "Non-interactive, for the whole group or for one option (the entry's disabled property): radio, label and description drop to the shared disabled colors."
}
],
"layout": {
"direction": "vertical",
"align": "start",
"justify": "start",
"wrap": false,
"sizing": "fill",
"notes": [
"The legend comes first, then the description, the options and the error message, separated by container.row-gap.",
"Options stack vertically, or flow on one line and wrap in the horizontal orientation, separated by container.gap for the orientation.",
"Within an option, the radio sits before the label, centered on its first line; the option's description sits under its label."
]
},
"accessibility": {
"ariaRole": "group",
"name": {
"text": "The group is named by its legend (the fieldset's first legend element on the web). Each option is named by its own label, associated as its label element; descriptions and the error message are exposed as descriptions of the radios, not as part of their names.",
"refs": [
"html-aam-fieldset-name",
"apg-naming-legends",
"wai-labels",
"html-aam-name-input",
"apg-describing"
]
},
"keyboard": [
{
"key": "Tab",
"action": "Moves focus into the group, onto the checked option, or onto the first option when none is checked; the next Tab leaves the group. Disabled options are skipped.",
"refs": [
"wcag:2.1.1",
"apg-radio-keyboard",
"html-disabled"
],
"platforms": [
"web"
]
},
{
"key": "Space",
"action": "Checks the focused option when it is not already checked.",
"refs": [
"apg-radio-keyboard"
],
"platforms": [
"web"
]
},
{
"key": "Arrow keys",
"action": "Right or Down moves focus to the next option and checks it, wrapping from the last to the first; Left or Up moves to the previous one and checks it, wrapping from the first to the last. The browser provides this for native radios sharing a name.",
"refs": [
"apg-radio-keyboard",
"html-radio-group"
],
"platforms": [
"web"
]
}
],
"guidelines": [
{
"text": "Expose the group, its name, each option's name and the checked state through the platform's native radio controls and grouping, so assistive technology receives them without extra work.",
"refs": [
"wcag:4.1.2",
"apg-radio",
"apg-radio-roles",
"compose-interactive-elements"
]
},
{
"text": "Use native input type=radio elements sharing one name inside a fieldset whose legend names the choice: the fieldset exposes the group role and its name, each radio its role and checked state, and the browser keeps one option checked. Never add aria-checked to a native radio.",
"refs": [
"html-aria-fieldset",
"html-aria-input-radio",
"html-aam-input-radio",
"html-radio-group",
"mdn-radio-group"
],
"platforms": [
"web"
]
},
{
"text": "The legend is visible and names the choice; each option has a visible label associated with its radio. Together they are the labels or instructions the group needs.",
"refs": [
"wcag:3.3.2",
"wcag:1.3.1",
"wai-grouping",
"wcag-h71",
"wai-labels",
"wcag-h44"
]
},
{
"text": "Help text and the error message are referenced by aria-describedby from every radio, so they are read as descriptions after an option's name.",
"refs": [
"wcag-aria1",
"apg-describing"
],
"platforms": [
"web"
]
},
{
"text": "Never rely on color alone: the chosen option is shown by its dot, an error is stated in text, not only by the red borders.",
"refs": [
"wcag:1.4.1",
"wcag-f81"
]
},
{
"text": "Legend, option labels and descriptions contrast at least 4.5:1 with the surface in every color mode and enabled state: 19.07:1 (primary text) and 12.87:1 (descriptions) in light mode, 11.27:1 and 8.80:1 in dark mode on the default surface; the error message 4.52:1 (light) and 4.97:1 (dark). Disabled text is inactive and exempt.",
"refs": [
"wcag:1.4.3"
]
},
{
"text": "Each radio is what identifies its option and shows its state, so its border, its fill and its dot contrast at least 3:1 with adjacent colors in every color mode: unchecked border 12.87:1 (light) and 8.80:1 (dark) on the default surface; checked fill 5.79:1 and 12.80:1; the dot on the fill 5.74:1 and 13.04:1; the error border 6.12:1 and 3.82:1; the focus ring 5.79:1 and 12.80:1. Disabled is exempt.",
"refs": [
"wcag:1.4.11",
"understanding-non-text-contrast-boundaries"
]
},
{
"text": "A visible focus indicator appears on the focused radio, drawn outside it.",
"refs": [
"wcag:2.4.7",
"wcag:2.4.13"
]
},
{
"text": "The focus ring is drawn with :focus-visible, so pointer focus shows no ring.",
"refs": [
"mdn-focus-visible"
],
"platforms": [
"web"
]
},
{
"text": "A radio alone is 16 or 20 CSS pixels, under the 24 pixel minimum; its label is part of the target and brings the option above it, and the spacing exception applies when options are 24 pixels apart.",
"refs": [
"wcag:2.5.8"
]
},
{
"text": "When no option is chosen in a required group, say so with aria-invalid on the radios and describe the error in text; the required attribute on the radios lets the browser enforce it.",
"refs": [
"wcag:3.3.1",
"wcag-aria21",
"wai-notifications",
"wcag-g83",
"wai-validation"
],
"platforms": [
"web"
]
},
{
"text": "Disabling the group with the fieldset's disabled attribute disables every radio and removes them from the focus order; a disabled option uses its own disabled attribute. When people need to know an option exists, keep it enabled and explain.",
"refs": [
"html-fieldset-disabled",
"html-disabled",
"apg-disabled-focus"
],
"platforms": [
"web"
]
},
{
"text": "Apple recommends a hit region of at least 44 x 44 pt; each option (radio and label) must reach it on iOS. Avoid relying solely on different colors to communicate the state.",
"refs": [
"hig-toggles-best-practices",
"hig-accessibility-mobility"
],
"platforms": [
"ios"
]
},
{
"text": "Android recommends a touch target of at least 48 x 48 dp; Material's RadioButton sets that minimum itself when it can receive actions. Lift the selection to the row with the selectable modifier and the RadioButton role, so label and radio form one element whose state is announced.",
"refs": [
"android-large-controls",
"compose-target-sizes",
"compose-interactive-elements",
"compose-state-description"
],
"platforms": [
"android"
]
},
{
"text": "Color transitions of the radios are removed when the user asks for reduced motion.",
"refs": [
"wcag:2.3.3",
"mdn-reduced-motion"
]
}
],
"references": [
{
"id": "apg-radio",
"title": "APG: Radio group pattern",
"url": "https://www.w3.org/WAI/ARIA/apg/patterns/radio/"
},
{
"id": "apg-radio-keyboard",
"title": "APG: Radio group pattern, keyboard interaction",
"url": "https://www.w3.org/WAI/ARIA/apg/patterns/radio/#keyboardinteraction"
},
{
"id": "apg-radio-roles",
"title": "APG: Radio group pattern, roles, states and properties",
"url": "https://www.w3.org/WAI/ARIA/apg/patterns/radio/#roles_states_properties"
},
{
"id": "apg-naming-legends",
"title": "APG: Naming fieldsets with legends",
"url": "https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/#naming_with_legends"
},
{
"id": "apg-describing",
"title": "APG: Describing with aria-describedby",
"url": "https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/#describing_with_aria-describedby"
},
{
"id": "apg-disabled-focus",
"title": "APG: Focusability of disabled controls",
"url": "https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_disabled_controls"
},
{
"id": "html-aria-fieldset",
"title": "ARIA in HTML: fieldset element (role group)",
"url": "https://www.w3.org/TR/html-aria/#el-fieldset"
},
{
"id": "html-aria-input-radio",
"title": "ARIA in HTML: input type=radio",
"url": "https://www.w3.org/TR/html-aria/#el-input-radio"
},
{
"id": "html-aam-input-radio",
"title": "HTML-AAM: input type=radio",
"url": "https://www.w3.org/TR/html-aam-1.0/#el-input-radio"
},
{
"id": "html-aam-fieldset-name",
"title": "HTML-AAM: fieldset accessible name computation (the legend)",
"url": "https://www.w3.org/TR/html-aam-1.0/#fieldset-element-accessible-name-computation"
},
{
"id": "html-aam-name-input",
"title": "HTML-AAM: accessible name computation of input elements (associated label element)",
"url": "https://www.w3.org/TR/html-aam-1.0/#input-type-text-input-type-password-input-type-number-input-type-search-input-type-tel-input-type-email-input-type-url-and-textarea-elements-accessible-name-computation"
},
{
"id": "html-radio-group",
"title": "HTML: radio button group (same name, one checked at a time)",
"url": "https://html.spec.whatwg.org/multipage/input.html#radio-button-group"
},
{
"id": "html-fieldset-disabled",
"title": "HTML: the fieldset disabled attribute",
"url": "https://html.spec.whatwg.org/multipage/form-elements.html#attr-fieldset-disabled"
},
{
"id": "html-disabled",
"title": "HTML: disabled form controls",
"url": "https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-disabled"
},
{
"id": "mdn-radio-group",
"title": "MDN: input type=radio, defining a radio group",
"url": "https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/radio#defining_a_radio_group"
},
{
"id": "mdn-focus-visible",
"title": "MDN: :focus-visible",
"url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:focus-visible"
},
{
"id": "mdn-reduced-motion",
"title": "MDN: prefers-reduced-motion",
"url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-reduced-motion"
},
{
"id": "wai-labels",
"title": "WAI tutorials: Labeling controls",
"url": "https://www.w3.org/WAI/tutorials/forms/labels/"
},
{
"id": "wai-grouping",
"title": "WAI tutorials: Grouping controls",
"url": "https://www.w3.org/WAI/tutorials/forms/grouping/"
},
{
"id": "wai-validation",
"title": "WAI tutorials: Validating input (the required attribute)",
"url": "https://www.w3.org/WAI/tutorials/forms/validation/"
},
{
"id": "wai-notifications",
"title": "WAI tutorials: User notifications (error messages)",
"url": "https://www.w3.org/WAI/tutorials/forms/notifications/"
},
{
"id": "wcag-h44",
"title": "WCAG technique H44: label elements to associate text labels with form controls",
"url": "https://www.w3.org/WAI/WCAG22/Techniques/html/H44"
},
{
"id": "wcag-h71",
"title": "WCAG technique H71: fieldset and legend for groups of form controls",
"url": "https://www.w3.org/WAI/WCAG22/Techniques/html/H71"
},
{
"id": "wcag-g83",
"title": "WCAG technique G83: text descriptions for required fields not completed",
"url": "https://www.w3.org/WAI/WCAG22/Techniques/general/G83"
},
{
"id": "wcag-aria1",
"title": "WCAG technique ARIA1: aria-describedby for a descriptive label",
"url": "https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA1"
},
{
"id": "wcag-aria21",
"title": "WCAG technique ARIA21: aria-invalid to indicate an error field",
"url": "https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA21"
},
{
"id": "wcag-f81",
"title": "WCAG failure F81: required or error fields identified by color only",
"url": "https://www.w3.org/WAI/WCAG22/Techniques/failures/F81"
},
{
"id": "understanding-non-text-contrast-boundaries",
"title": "Understanding WCAG 1.4.11 Non-text Contrast: user interface components and boundaries",
"url": "https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html#boundaries"
},
{
"id": "hig-toggles-best-practices",
"title": "Apple HIG: Toggles, best practices (checkboxes and switches)",
"url": "https://developer.apple.com/design/human-interface-guidelines/toggles#Best-practices"
},
{
"id": "hig-accessibility-mobility",
"title": "Apple HIG: Accessibility, mobility (control sizes)",
"url": "https://developer.apple.com/design/human-interface-guidelines/accessibility#Mobility"
},
{
"id": "android-large-controls",
"title": "Android accessibility: use large, simple controls",
"url": "https://developer.android.com/guide/topics/ui/accessibility/apps#large-controls"
},
{
"id": "compose-target-sizes",
"title": "Jetpack Compose accessibility: minimum touch target sizes",
"url": "https://developer.android.com/develop/ui/compose/accessibility/api-defaults#minimum-target-sizes"
},
{
"id": "compose-interactive-elements",
"title": "Jetpack Compose accessibility: interactive elements (toggleable, selection controls)",
"url": "https://developer.android.com/develop/ui/compose/accessibility/api-defaults#interactive-elements"
},
{
"id": "compose-state-description",
"title": "Jetpack Compose accessibility: state description",
"url": "https://developer.android.com/develop/ui/compose/accessibility/semantics#state-description"
}
]
},
"content": {
"guidelines": [
"The legend is the question or the thing chosen, as a noun phrase or a short question: Delivery method, How often should we email you?",
"Option labels are short, parallel and mutually exclusive: Standard, Express, Pickup. Sentence case, no trailing punctuation.",
"Put costs, delays and consequences in an option's description, not in its label.",
"Order options logically (most common first, or a natural scale); offer a neutral option (None, Not sure) when people may have no preference.",
"The error message says what to do: Choose a delivery method."
]
},
"examples": [
{
"id": "vertical-medium",
"name": "Vertical, medium",
"props": {
"orientation": "vertical",
"size": "medium",
"label": "Delivery method",
"name": "delivery",
"options": [
{
"label": "Standard",
"value": "standard",
"description": "3 to 5 business days, free",
"checked": true
},
{
"label": "Express",
"value": "express",
"description": "1 to 2 business days"
},
{
"label": "Pickup",
"value": "pickup",
"description": "Today, at the store"
}
]
}
},
{
"id": "horizontal-medium",
"name": "Horizontal, medium",
"props": {
"orientation": "horizontal",
"size": "medium",
"label": "Do you have an account?",
"name": "account",
"options": [
{
"label": "Yes",
"value": "yes"
},
{
"label": "No",
"value": "no"
},
{
"label": "Not sure",
"value": "unsure"
}
]
}
},
{
"id": "vertical-small",
"name": "Vertical, small",
"props": {
"orientation": "vertical",
"size": "small",
"label": "Sort by",
"name": "sort",
"options": [
{
"label": "Relevance",
"value": "relevance",
"checked": true
},
{
"label": "Newest first",
"value": "date"
},
{
"label": "Price",
"value": "price"
}
]
}
},
{
"id": "vertical-medium-description",
"name": "Vertical, medium, with description and a disabled option",
"props": {
"orientation": "vertical",
"size": "medium",
"label": "Plan",
"description": "You can change plans at any time.",
"name": "plan",
"options": [
{
"label": "Free",
"value": "free",
"checked": true
},
{
"label": "Team",
"value": "team",
"description": "Up to 20 members"
},
{
"label": "Enterprise",
"value": "enterprise",
"description": "Contact sales",
"disabled": true
}
]
}
},
{
"id": "vertical-medium-invalid",
"name": "Vertical, medium, required and invalid",
"description": "No option chosen in a required group; the error message says what to do.",
"props": {
"orientation": "vertical",
"size": "medium",
"label": "Delivery method",
"name": "delivery-invalid",
"required": true,
"invalid": true,
"errorMessage": "Choose a delivery method.",
"options": [
{
"label": "Standard",
"value": "standard"
},
{
"label": "Express",
"value": "express"
}
]
}
},
{
"id": "vertical-medium-disabled",
"name": "Vertical, medium, disabled",
"props": {
"orientation": "vertical",
"size": "medium",
"label": "Billing cycle",
"description": "Managed by your organization.",
"name": "billing",
"disabled": true,
"options": [
{
"label": "Monthly",
"value": "monthly"
},
{
"label": "Yearly",
"value": "yearly",
"checked": true
}
]
}
}
],
"tokens": {
"source": "packages/tokens/src/components/radio-group.tokens.json",
"prefix": "radio-group"
},
"implementations": {
"web": {
"css": "packages/components/src/components/radio-group/radio-group.css",
"template": "packages/components/src/components/radio-group/radio-group.template.mjs",
"selector": ".ds-radio-group",
"element": "fieldset",
"attributes": {
"orientation": "data-orientation",
"size": "data-size",
"disabled": "disabled",
"invalid": "data-invalid"
},
"stateAttribute": "data-state",
"partSelectors": {
"container": ".ds-radio-group",
"legend": ".ds-radio-group__legend",
"description": ".ds-radio-group__description",
"option": ".ds-radio-group__option",
"control": ".ds-radio-group__control",
"option-label": ".ds-radio-group__option-label",
"option-description": ".ds-radio-group__option-description",
"error-message": ".ds-radio-group__error-message"
}
}
},
"related": [
"checkbox",
"select",
"switch"
],
"decisions": [
{
"id": "group-of-native-radios",
"date": "2026-10-03",
"version": "0.1.0",
"area": "anatomy",
"status": "active",
"text": "The component is the whole group: a legend, optional help text, the options as a repeated part (label, value, description, checked, disabled) and an error message. On the web it is a fieldset of native radio buttons sharing one name; no single Radio component exists.",
"rationale": "A radio button never stands alone: the choice is the group, and the native group gives the arrow-key behaviour and the one-checked rule for free.",
"refs": [
"apg-radio",
"html-radio-group"
]
},
{
"id": "selection-control-colors",
"date": "2026-10-03",
"version": "0.1.0",
"area": "color",
"status": "active",
"text": "The radio follows the Checkbox's selection control colors: accent neutral border on the default surface when unchecked; accent brand fill with an on-accent brand dot when checked; hover tints the empty circle with the surface hover color; invalid takes the base error border; disabled the shared disabled colors.",
"tokens": [
"radio-group.control.background.*",
"radio-group.control.border-color.*",
"radio-group.control.color.*"
],
"refs": [
"wcag:1.4.11"
]
},
{
"id": "orientations",
"date": "2026-10-03",
"version": "0.1.0",
"area": "layout",
"status": "active",
"text": "Options stack vertically by default, 8 px apart; the horizontal orientation puts them on one wrapping line, 16 px apart.",
"tokens": [
"radio-group.container.gap.*"
]
},
{
"id": "sizes",
"date": "2026-10-03",
"version": "0.1.0",
"area": "size",
"status": "active",
"text": "Two sizes: small (16 px radio, 14 px label) and medium (20 px radio, 16 px label, the default), as the Checkbox; the radio border is 1.2 px (stroke-width.md) and fully round.",
"tokens": [
"radio-group.control.size.*",
"radio-group.control.border-width",
"radio-group.control.border-radius",
"radio-group.option-label.typography.*",
"radio-group.option-description.typography.*"
]
}
],
"changelog": [
{
"version": "0.1.0",
"date": "2026-10-03",
"note": "Initial draft (decisions group-of-native-radios, selection-control-colors, orientations, sizes). Fieldset of native radios with legend, description, option descriptions and error message; states default, hover, focus-visible, checked, invalid, disabled on the option part; orientations vertical and horizontal; sizes small and medium. Contrast on the default surface, light / dark: legend and labels 19.07 / 11.27, descriptions 12.87 / 8.80, error text 4.52 / 4.97, unchecked border 12.87 / 8.80, checked fill 5.79 / 12.80, dot on fill 5.74 / 13.04, error border 6.12 / 3.82, focus ring 5.79 / 12.80."
}
]
}
Follows the Color foundation (20 tokens), the Focus ring foundation (3 tokens), the Motion foundation (2 tokens), the Shape foundation (2 tokens), the Spacing and sizing foundation (7 tokens), the Typography foundation (7 tokens).
Anatomy
| Part | Role | Required | Description | Web selector |
|---|---|---|---|---|
container |
container |
yes | Root: the group of options (a fieldset on the web). Carries the orientation, the size and the states forced for review. | .ds-radio-group |
legend |
label |
yes | Text naming the choice. Provides the group's accessible name. | .ds-radio-group__legend |
description |
description |
no | Optional help text under the legend, associated with every option as its description. | .ds-radio-group__description |
option (repeatable) |
container |
yes | One option: its radio, label and description on a grid. Takes the states of the control it holds. | .ds-radio-group__option |
control |
indicator |
yes | The radio: the native radio button, drawn by the component. Receives focus, holds the checked state and shows it with a dot. | .ds-radio-group__control |
option-label |
label |
yes | Text naming the option; activating it checks the radio. | .ds-radio-group__option-label |
option-description |
description |
no | Optional help text under the option's label. | .ds-radio-group__option-description |
error-message |
errormessage |
no | Text describing the error when the group is invalid (no option chosen in a required group). Associated with every option as a description. Shown when invalid |
.ds-radio-group__error-message |
Hover or focus a row to outline the part in the rendering above; hover the rendering to find its row.
Properties
| Property | Type | Options / default | Binds to / affects | Description |
|---|---|---|---|---|
orientation |
enum |
|
affects
containerweb:
data-orientation |
How the options flow. |
size |
enum |
|
affects
optioncontroloption-labeloption-descriptionweb:
data-size |
Radio size, option label and description text size scale together. |
label required |
string | — |
binds
legend |
The question or the thing being chosen (Delivery method). |
description |
string | — |
binds
description |
Help text for the whole choice. |
name |
string | — | Form field name shared by every option, which is what makes them one group. Defaults to the group's id. | |
options required |
list |
entries: option
|
binds
option |
The options, in order. |
errorMessage |
string | — |
binds
error-message |
What is wrong and what to do, shown when the group is invalid. |
required |
boolean |
false
|
An option must be chosen to submit the form. | |
invalid |
boolean |
false
|
web:
data-invalid |
Validation failed: every radio takes the error border and the error message is shown. |
disabled |
boolean |
false
|
web:
disabled |
Disables the whole group; the chosen option stays visible. |
Part properties
Set on each instance of a repeated part, through the list property holding it.
| Property | Type | Options / default | Binds to / affects | Description |
|---|---|---|---|---|
option.label required |
string | — |
binds
option-label |
Name of the option. Provides the radio's accessible name. |
option.value |
string | — | Value submitted when the option is checked. Defaults to the option's position. | |
option.description |
string | — |
binds
option-description |
Help text under the option's label, exposed as the radio's description. |
option.checked |
boolean |
false
|
Whether this option is the chosen one. At most one option is checked. | |
option.disabled |
boolean |
false
|
The option stays visible, dimmed, and cannot be chosen. |
States
| State | Trigger | Parts | Applies to | Description |
|---|---|---|---|---|
default |
interaction | option |
every platform and input | Resting, unchecked: an outlined empty circle. |
hover |
interaction | option |
input: pointer | Pointer over the radio or its label. The empty circle takes a light tint; a checked radio does not change. Needs a pointing device: touch has no hover. |
focus-visible |
interaction | option |
input: keyboard | Keyboard focus on a radio. Shows the shared focus ring outside it; pointer focus shows none. |
checked |
property (options) |
option |
every platform and input | The chosen option (the entry's checked property): the circle fills with the brand color and shows a dot. |
invalid |
property (invalid) |
option |
every platform and input | Validation failed on the group: every radio border takes the error color and the error message is shown. |
disabled |
property (disabled) |
option |
every platform and input | Non-interactive, for the whole group or for one option (the entry's disabled property): radio, label and description drop to the shared disabled colors. |
Layout
direction verticalalign startjustify startwrap falsesizing fill
- The legend comes first, then the description, the options and the error message, separated by container.row-gap.
- Options stack vertically, or flow on one line and wrap in the horizontal orientation, separated by container.gap for the orientation.
- Within an option, the radio sits before the label, centered on its first line; the option's description sits under its label.
Accessibility
Target: WCAG 2.2 level AA. Every statement cites the trusted source it comes from.
ARIA role group.
Accessible name: The group is named by its legend (the fieldset's first legend element on the web). Each option is named by its own label, associated as its label element; descriptions and the error message are exposed as descriptions of the radios, not as part of their names. HTML-AAM: fieldset accessible name computation (the legend)APG: Naming fieldsets with legendsWAI tutorials: Labeling controlsHTML-AAM: accessible name computation of input elements (associated label element)APG: Describing with aria-describedby
| Key | Action | Platforms | Sources |
|---|---|---|---|
| Tab | Moves focus into the group, onto the checked option, or onto the first option when none is checked; the next Tab leaves the group. Disabled options are skipped. | Web | WCAG 2.1.1 Keyboard (A)APG: Radio group pattern, keyboard interactionHTML: disabled form controls |
| Space | Checks the focused option when it is not already checked. | Web | APG: Radio group pattern, keyboard interaction |
| Arrow keys | Right or Down moves focus to the next option and checks it, wrapping from the last to the first; Left or Up moves to the previous one and checks it, wrapping from the first to the last. The browser provides this for native radios sharing a name. | Web | APG: Radio group pattern, keyboard interactionHTML: radio button group (same name, one checked at a time) |
Every platform
- Expose the group, its name, each option's name and the checked state through the platform's native radio controls and grouping, so assistive technology receives them without extra work. WCAG 4.1.2 Name, Role, Value (A)APG: Radio group patternAPG: Radio group pattern, roles, states and propertiesJetpack Compose accessibility: interactive elements (toggleable, selection controls)
- The legend is visible and names the choice; each option has a visible label associated with its radio. Together they are the labels or instructions the group needs. WCAG 3.3.2 Labels or Instructions (A)WCAG 1.3.1 Info and Relationships (A)WAI tutorials: Grouping controlsWCAG technique H71: fieldset and legend for groups of form controlsWAI tutorials: Labeling controlsWCAG technique H44: label elements to associate text labels with form controls
- Never rely on color alone: the chosen option is shown by its dot, an error is stated in text, not only by the red borders. WCAG 1.4.1 Use of Color (A)WCAG failure F81: required or error fields identified by color only
- Legend, option labels and descriptions contrast at least 4.5:1 with the surface in every color mode and enabled state: 19.07:1 (primary text) and 12.87:1 (descriptions) in light mode, 11.27:1 and 8.80:1 in dark mode on the default surface; the error message 4.52:1 (light) and 4.97:1 (dark). Disabled text is inactive and exempt. WCAG 1.4.3 Contrast (Minimum) (AA)
- Each radio is what identifies its option and shows its state, so its border, its fill and its dot contrast at least 3:1 with adjacent colors in every color mode: unchecked border 12.87:1 (light) and 8.80:1 (dark) on the default surface; checked fill 5.79:1 and 12.80:1; the dot on the fill 5.74:1 and 13.04:1; the error border 6.12:1 and 3.82:1; the focus ring 5.79:1 and 12.80:1. Disabled is exempt. WCAG 1.4.11 Non-text Contrast (AA)Understanding WCAG 1.4.11 Non-text Contrast: user interface components and boundaries
- A visible focus indicator appears on the focused radio, drawn outside it. WCAG 2.4.7 Focus Visible (AA)WCAG 2.4.13 Focus Appearance (AAA)
- A radio alone is 16 or 20 CSS pixels, under the 24 pixel minimum; its label is part of the target and brings the option above it, and the spacing exception applies when options are 24 pixels apart. WCAG 2.5.8 Target Size (Minimum) (AA)
- Color transitions of the radios are removed when the user asks for reduced motion. WCAG 2.3.3 Animation from Interactions (AAA)MDN: prefers-reduced-motion
Web
- Use native input type=radio elements sharing one name inside a fieldset whose legend names the choice: the fieldset exposes the group role and its name, each radio its role and checked state, and the browser keeps one option checked. Never add aria-checked to a native radio. ARIA in HTML: fieldset element (role group)ARIA in HTML: input type=radioHTML-AAM: input type=radioHTML: radio button group (same name, one checked at a time)MDN: input type=radio, defining a radio group
- Help text and the error message are referenced by aria-describedby from every radio, so they are read as descriptions after an option's name. WCAG technique ARIA1: aria-describedby for a descriptive labelAPG: Describing with aria-describedby
- The focus ring is drawn with :focus-visible, so pointer focus shows no ring. MDN: :focus-visible
- When no option is chosen in a required group, say so with aria-invalid on the radios and describe the error in text; the required attribute on the radios lets the browser enforce it. WCAG 3.3.1 Error Identification (A)WCAG technique ARIA21: aria-invalid to indicate an error fieldWAI tutorials: User notifications (error messages)WCAG technique G83: text descriptions for required fields not completedWAI tutorials: Validating input (the required attribute)
- Disabling the group with the fieldset's disabled attribute disables every radio and removes them from the focus order; a disabled option uses its own disabled attribute. When people need to know an option exists, keep it enabled and explain. HTML: the fieldset disabled attributeHTML: disabled form controlsAPG: Focusability of disabled controls
iOS
- Apple recommends a hit region of at least 44 x 44 pt; each option (radio and label) must reach it on iOS. Avoid relying solely on different colors to communicate the state. Apple HIG: Toggles, best practices (checkboxes and switches)Apple HIG: Accessibility, mobility (control sizes)
Android
- Android recommends a touch target of at least 48 x 48 dp; Material's RadioButton sets that minimum itself when it can receive actions. Lift the selection to the row with the selectable modifier and the RadioButton role, so label and radio form one element whose state is announced. Android accessibility: use large, simple controlsJetpack Compose accessibility: minimum touch target sizesJetpack Compose accessibility: interactive elements (toggleable, selection controls)Jetpack Compose accessibility: state description
WCAG success criteria
| Criterion | Level | Understanding |
|---|---|---|
| 1.3.1 Info and Relationships | A | Understanding 1.3.1 |
| 1.4.1 Use of Color | A | Understanding 1.4.1 |
| 1.4.3 Contrast (Minimum) | AA | Understanding 1.4.3 |
| 1.4.11 Non-text Contrast | AA | Understanding 1.4.11 |
| 2.1.1 Keyboard | A | Understanding 2.1.1 |
| 2.3.3 Animation from Interactions | AAA (beyond the AA target) | Understanding 2.3.3 |
| 2.4.7 Focus Visible | AA | Understanding 2.4.7 |
| 2.4.13 Focus Appearance | AAA (beyond the AA target) | Understanding 2.4.13 |
| 2.5.8 Target Size (Minimum) | AA | Understanding 2.5.8 |
| 3.3.1 Error Identification | A | Understanding 3.3.1 |
| 3.3.2 Labels or Instructions | A | Understanding 3.3.2 |
| 4.1.2 Name, Role, Value | A | Understanding 4.1.2 |
References
- APG: Radio group pattern W3C WAI-ARIA Authoring Practices Guide (APG) · guidance
- APG: Radio group pattern, keyboard interaction W3C WAI-ARIA Authoring Practices Guide (APG) · guidance
- APG: Radio group pattern, roles, states and properties W3C WAI-ARIA Authoring Practices Guide (APG) · guidance
- APG: Naming fieldsets with legends W3C WAI-ARIA Authoring Practices Guide (APG) · guidance
- APG: Describing with aria-describedby W3C WAI-ARIA Authoring Practices Guide (APG) · guidance
- APG: Focusability of disabled controls W3C WAI-ARIA Authoring Practices Guide (APG) · guidance
- ARIA in HTML: fieldset element (role group) W3C ARIA in HTML · standard
- ARIA in HTML: input type=radio W3C ARIA in HTML · standard
- HTML-AAM: input type=radio W3C HTML Accessibility API Mappings (HTML-AAM) · standard
- HTML-AAM: fieldset accessible name computation (the legend) W3C HTML Accessibility API Mappings (HTML-AAM) · standard
- HTML-AAM: accessible name computation of input elements (associated label element) W3C HTML Accessibility API Mappings (HTML-AAM) · standard
- HTML: radio button group (same name, one checked at a time) WHATWG HTML Living Standard · standard
- HTML: the fieldset disabled attribute WHATWG HTML Living Standard · standard
- HTML: disabled form controls WHATWG HTML Living Standard · standard
- MDN: input type=radio, defining a radio group MDN Web Docs · reference
- MDN: :focus-visible MDN Web Docs · reference
- MDN: prefers-reduced-motion MDN Web Docs · reference
- WAI tutorials: Labeling controls W3C WAI tutorials · guidance
- WAI tutorials: Grouping controls W3C WAI tutorials · guidance
- WAI tutorials: Validating input (the required attribute) W3C WAI tutorials · guidance
- WAI tutorials: User notifications (error messages) W3C WAI tutorials · guidance
- WCAG technique H44: label elements to associate text labels with form controls W3C WAI Understanding and Techniques for WCAG 2.2 · guidance
- WCAG technique H71: fieldset and legend for groups of form controls W3C WAI Understanding and Techniques for WCAG 2.2 · guidance
- WCAG technique G83: text descriptions for required fields not completed W3C WAI Understanding and Techniques for WCAG 2.2 · guidance
- WCAG technique ARIA1: aria-describedby for a descriptive label W3C WAI Understanding and Techniques for WCAG 2.2 · guidance
- WCAG technique ARIA21: aria-invalid to indicate an error field W3C WAI Understanding and Techniques for WCAG 2.2 · guidance
- WCAG failure F81: required or error fields identified by color only W3C WAI Understanding and Techniques for WCAG 2.2 · guidance
- Understanding WCAG 1.4.11 Non-text Contrast: user interface components and boundaries W3C WAI Understanding and Techniques for WCAG 2.2 · guidance
- Apple HIG: Toggles, best practices (checkboxes and switches) Apple Human Interface Guidelines and accessibility documentation · platform
- Apple HIG: Accessibility, mobility (control sizes) Apple Human Interface Guidelines and accessibility documentation · platform
- Android accessibility: use large, simple controls Android accessibility documentation · platform
- Jetpack Compose accessibility: minimum touch target sizes Android accessibility documentation · platform
- Jetpack Compose accessibility: interactive elements (toggleable, selection controls) Android accessibility documentation · platform
- Jetpack Compose accessibility: state description Android accessibility documentation · platform
Content
- The legend is the question or the thing chosen, as a noun phrase or a short question: Delivery method, How often should we email you?
- Option labels are short, parallel and mutually exclusive: Standard, Express, Pickup. Sentence case, no trailing punctuation.
- Put costs, delays and consequences in an option's description, not in its label.
- Order options logically (most common first, or a natural scale); offer a neutral option (None, Not sure) when people may have no preference.
- The error message says what to do: Choose a delivery method.
Changelog
0.1.02026-10-03 — Initial draft (decisions group-of-native-radios, selection-control-colors, orientations, sizes). Fieldset of native radios with legend, description, option descriptions and error message; states default, hover, focus-visible, checked, invalid, disabled on the option part; orientations vertical and horizontal; sizes small and medium. Contrast on the default surface, light / dark: legend and labels 19.07 / 11.27, descriptions 12.87 / 8.80, error text 4.52 / 4.97, unchecked border 12.87 / 8.80, checked fill 5.79 / 12.80, dot on fill 5.74 / 13.04, error border 6.12 / 3.82, focus ring 5.79 / 12.80.