Skip to content

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

PartRoleRequiredDescriptionWeb 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

PropertyTypeOptions / defaultBinds to / affectsDescription
orientation enum
  • vertical (default) — One option per line, the default: easiest to scan and leaves room for descriptions.
  • horizontal — Options on one line, wrapping when needed: two or three short options (Yes / No).
affects container
web: data-orientation
How the options flow.
size enum
  • small — Dense lists and tables.
  • medium (default) — Default.
affects optioncontroloption-labeloption-description
web: 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.

PropertyTypeOptions / defaultBinds to / affectsDescription
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

StateTriggerPartsApplies toDescription
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

KeyActionPlatformsSources
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

Web

iOS

Android

WCAG success criteria

CriterionLevelUnderstanding
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

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.0 2026-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.