---
title: "Radio group"
description: "Asks one question that has exactly one answer among a few options shown all at once — a set of radio buttons that holds the chosen value, under a label that can say the answer is required or optional, with a description and one error for the set."
url: "https://tanrei.chnotchy.com/components/radio-group/"
last_updated: 2026-10-23T21:17:12.000Z
---

# Radio group

> Asks one question that has exactly one answer among a few options shown all at once — a set of radio buttons that holds the chosen value, under a label that can say the answer is required or optional, with a description and one error for the set.

## Overview

A [radio button](https://tanrei.chnotchy.com/components/radio-button/) is never used alone. A radio group makes several of them one question: it gives the set a name, a line that says how to answer, a word that says whether it must be answered, and one reason when the answer is refused — and it holds the answer as one value.

**Built from** a [field group](https://tanrei.chnotchy.com/components/field-group/) and [radio buttons](https://tanrei.chnotchy.com/components/radio-button/). The field group is the frame, in its radio-group kind: the label, the required or optional mark, the description, the error, the layout in a column or a row — and the one radio group that assistive technology is told of. The radio buttons hold the value: each is an option with its own label, and which of them is selected is the answer. The radio group draws nothing of its own and has no token. What it adds to the two is the state every product otherwise writes: one value in and out and one `change` for it, the arrow keys and the one stop in the focus order kept for the set, and a place in a form as one control.

It is as wide as the place it is put in.

On the web it is `<tanrei-radio-group>`. The other platforms have not implemented it yet.

### Usage

**Web**

```html
<tanrei-radio-group label="Delivery" name="delivery" value="standard" required required-text="Required"
                    supporting-text="You can change this until the order ships.">
  <tanrei-radio-button value="standard" label="Standard, 3 to 5 days"></tanrei-radio-button>
  <tanrei-radio-button value="express" label="Express, next day"></tanrei-radio-button>
  <tanrei-radio-button value="pickup" label="Pick up in store"></tanrei-radio-button>
</tanrei-radio-group>

<script type="module">
  import '@tanrei/web/components/radio-group';

  const group = document.querySelector('tanrei-radio-group');
  group.addEventListener('change', (event) => {
    // event.detail.value is the new value; group.value already holds it.
  });
</script>
```

**React**

```tsx
import { RadioButton, RadioGroup } from '@tanrei/react';

<RadioGroup label="Delivery" value={delivery} onChange={(event) => setDelivery(event.detail.value)}
            error={refused} errorText="Express delivery does not reach this address. Choose another way.">
  <RadioButton value="standard" label="Standard, 3 to 5 days" />
  <RadioButton value="express" label="Express, next day" />
  <RadioButton value="pickup" label="Pick up in store" />
</RadioGroup>
```

Inside a radio group a radio button needs no `name` and no `checked`: the group’s `value` says which one is selected.

### One level down

The same set, written by hand from its two members, is the same picture and the same radio group to assistive technology. The field group is told what its set is with `kind="radio-group"`, and the radio buttons carry one `name`:

```html
<tanrei-field-group kind="radio-group" label="Delivery"
                    supporting-text="You can change this until the order ships."
                    required required-text="Required">
  <tanrei-radio-button name="delivery" value="standard" label="Standard, 3 to 5 days" checked></tanrei-radio-button>
  <tanrei-radio-button name="delivery" value="express" label="Express, next day"></tanrei-radio-button>
  <tanrei-radio-button name="delivery" value="pickup" label="Pick up in store"></tanrei-radio-button>
</tanrei-field-group>
```

What the field group and the radio buttons written by hand do not do is hold the answer. Nothing that is drawn or said differs.

| Aspect | Written by hand | The radio group |
| --- | --- | --- |
| The answer | Read from the radio buttons by the product | `value`, and one `change` on one element |
| Setting the answer | `checked` on one radio button | `value = "express"` |
| What a form submits | The same entry, by the selected radio button | The same entry, by the group |
| A required set left unanswered | Drawn and said as required; the form does not refuse it | The form’s own validation refuses it |
| The selected option is removed, or not shown yet | The answer is lost | `value` is kept |
| A selected option you disabled by itself | Submits nothing | Its value is submitted |
| Arrow keys, one stop for the Tab key, each radio button’s position in the set | The same, by the radio buttons of one name | The same, by the group |

Write the field group and the radio buttons by hand where the set holds something between its options that the group does not lay out — a line of your own under each option — or where the options must stand in your own elements in a way a slot does not allow.

## Specs

- Category: Selection
- Level: composed
- Listing: default
- Built from: Field group, Radio button
- Page: https://tanrei.chnotchy.com/components/radio-group/
- Spec: https://github.com/chnotchy/tanrei/blob/main/specs/radio-group.spec.json

### Availability

| Design system | Has it | Notes |
| --- | --- | --- |
| Tanrei (`tanrei`) | Yes | — |
| Cupertino (Apple HIG-inspired) (`cupertino`) | Yes | — |
| M3 Expressive-like (Material 3 Expressive-inspired) (`m3-expressive-like`) | Yes | — |
| M3-like (Material 3-inspired) (`m3-like`) | Yes | — |

### Platforms

- Web: the element `<tanrei-radio-group>`, registered by importing `@tanrei/web` (or only its own module, `@tanrei/web/components/radio-group`).
- React: `RadioGroup` from `@tanrei/react`.

| Platform | Name | Status |
| --- | --- | --- |
| Web | `tanrei-radio-group` | lagging |
| SwiftUI | `TanreiRadioGroup` | planned |
| Jetpack Compose | `TanreiRadioGroup` | planned |
| Flutter | `TanreiRadioGroup` | planned |
| React Native | `RadioGroup` | planned |

Names a platform gives in its own idiom:

| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- | --- |
| `value` (prop) | `value` | `selection` | `value` | `groupValue` | `value` |
| `label` (prop) | `label` | `label` | `label` | `labelText` | `label` |
| `labelHidden` (prop) | `labelHidden` | `labelsHidden` | `labelHidden` | `labelHidden` | `labelHidden` |
| `supportingText` (prop) | `supportingText` | `supportingText` | `supportingText` | `helperText` | `supportingText` |
| `error` (prop) | `error` | `error` | `isError` | `isError` | `error` |
| `disabled` (prop) | `disabled` | `disabled` | `enabled` | `enabled` | `disabled` |
| `required` (prop) | `required` | `isRequired` | `required` | `required` | `required` |
| `optional` (prop) | `optional` | `isOptional` | `optional` | `optional` | `optional` |
| `change` (event) | `change` | `selection` | `onValueChange` | `onChanged` | `onValueChange` |

Statuses: `planned` — In the inventory. Nothing shippable exists yet. `lagging` — An implementation of an earlier spec exists; it has not caught up with the current one. `alpha` — Implemented with tests. The API may still change. `beta` — Feature complete and at parity. Only fixes are expected. `stable` — Covered by semantic versioning. `deprecated` — Still shipped; scheduled for removal.

### Anatomy

- `field-group`: The field group, in its radio-group kind: the label with its requirement mark, the description under it, the column or the row the options stand in, and the error text below the last option — and the one radio group that assistive technology is told of. The field group itself, with its own tokens, not a drawing of it.
- `radio-button`: The radio buttons of the set, in the order given: the product's own, each the Radio button component with its own label, drawn from its own tokens. Which of them is selected is the group's value.

### Props

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `value` | `string` | `""` | `value` | The value of the selected radio button; the empty string while none is selected. The radio group sets it when a person selects an option, and emits change; the product may also set it. |
| `label` | `string` | `""` | `label` | Names the set, visibly and to assistive technology: the question its controls answer ("Notify me by"). Required. (From Field group.) |
| `labelHidden` | `boolean` | `false` | `label-hidden` | Does not draw the label, which stays the accessible name of the group. For a set whose visible heading is elsewhere. (From Field group.) |
| `supportingText` | `string` | `""` | `supporting-text` | The description: how to answer ("Choose all that apply"), drawn under the label and given to assistive technology as the group's description. (From Field group.) |
| `errorText` | `string` | `""` | `error-text` | What is wrong with the set as a whole and how to put it right ("Choose at least one"), drawn below the controls while error is true. (From Field group: On the web it is also what the form's own validation says of a required group left unanswered, where the browser has no sentence of its own for it (RGP-9).) |
| `error` | `boolean` | `false` | `error` | Puts the set in the error condition: the error text is drawn, and each selection control that has an error condition of its own (a checkbox, a radio button) is drawn in it. What an answer means is the product's to validate; the group never sets this itself. One control of a valid group may still be in error by itself. (From Field group: The radio group is exposed as invalid, and its radio buttons are drawn as they are: a radio button has no error condition (RGP-8).) |
| `disabled` | `boolean` | `false` | `disabled` | Disables every control of the set and draws the frame as disabled. (From Field group: The value is kept, and on the web it is not submitted (RGP-7, RGP-9).) |
| `readOnly` | `boolean` | `false` | `readonly` | Shows the answer and keeps it: the group can be focused and its options read, its value is submitted, but the person cannot change it (RGP-20). |
| `required` | `boolean` | `false` | `required` | What the form asks of the set: true exposes it as required and draws the required mark beside the label. It never validates: a missing answer is the product's to report with error. (From Field group: exposes the radio group as required, and on the web an empty value is reported to the form's own validation (RGP-9, RGP-10).) |
| `optional` | `boolean` | `false` | `optional` | Draws the optional tag after the label and adds optionalText to the group's description, because assistive technology has no state for it. Ignored while required is true: a set is never both. (From Field group.) |
| `requiredMark` | `enum`: `auto`, `tag`, `asterisk`, `none` | `"auto"` | `required-mark` | How this set shows that it is required: tag is a small filled Tag with requiredText, asterisk is "*" after the label, none draws nothing and keeps the semantics. Auto takes comp.field-group.requirement.mark, which follows sys.behavior.requirement-mark, the system's one answer. Ignored unless required is true. (From Field group.) |
| `requiredText` | `string` | `""` | `required-text` | The text of the required tag, in the product's language ("Required", "必須"). The product's: Tanrei has no word for it. Empty reads the product's words; with neither, FGR-24 says what is drawn. (From Field group: Not part of the radio group's description: its required state carries it (RGP-10).) |
| `optionalText` | `string` | `""` | `optional-text` | The text of the optional tag, in the product's language ("Optional", "任意"); also read to assistive technology as part of the description. The product's: Tanrei has no word for it. Empty reads the product's words. (From Field group.) |
| `orientation` | `enum`: `vertical`, `horizontal` | `"vertical"` | `orientation` | vertical stacks the controls in one column, the default and the form to prefer: every label starts on one edge and is read down. horizontal lays them out in a row that wraps, for two or three short options or a row of fields. (From Field group: The arrow keys are the same in both (RGP-5).) |

### Events

| Event | Description |
| --- | --- |
| `change` | The person selected another option; carries the new value, which the group already holds. Emitted once per selection, by the group and not by its radio buttons. Not emitted when the product sets value, or when the selected option is activated again. |

### Item: `radio-button`

An option of the set: the radio button component with its own label, selected by the group's value, placed, spaced and disabled by the group, otherwise drawn and pressed as it is anywhere.

It is a Radio button.

Names: Web `tanrei-radio-button`, SwiftUI `TanreiRadioButton`, Jetpack Compose `TanreiRadioButton`, Flutter `TanreiRadioButton`, React Native `RadioButton`.

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `value` | `string` | `""` | `value` | What the option stands for, as the Radio button's value: the group's value while this option is the selected one. Unique in the group and not empty. |
| `label` | `string` | `""` | `label` | Its own label, as the Radio button's label. |
| `disabled` | `boolean` | `false` | `disabled` | As the Radio button's disabled: one option of an enabled group may be disabled by itself, and is then skipped by the arrow keys. |

### States

`enabled`, `disabled`, `error`

### Accessibility

- Role: `radiogroup`
- Labeling: label names the radio group, drawn or not (labelHidden). Each option keeps its own name, its own label beside it, and says its position in the set. The group's description is supportingText, then errorText while error is true, then optionalText while it is optional; a required group carries the required state, and a group in error the invalid state, so neither is said in words a second time. The requirement tag and the asterisk are hidden.
- Minimum target size: yes

| Keys | Action |
| --- | --- |
| Tab / Shift+Tab | Moves focus into the group — to the selected option, or to the first enabled one while none is selected — and out of it. The group has one stop and is not a stop itself (RGP-6). |
| Down Arrow / Right Arrow | Moves to the next enabled option and selects it, wrapping from the last to the first; in a right-to-left layout Left Arrow does what Right Arrow does here (RGP-5). While the group is read-only it moves focus and selects nothing (RGP-20). |
| Up Arrow / Left Arrow | Moves to the previous enabled option and selects it, wrapping from the first to the last; in a right-to-left layout Right Arrow does what Left Arrow does here. While the group is read-only it moves focus and selects nothing. |
| Space | Selects the focused option if it is not selected (after Tab into a group with nothing selected); does nothing on the selected one. Nothing while the group is read-only. |
| Enter | Nothing on the web, where it submits the form; selects where the platform activates any focused control with Enter (Android, tvOS), as RAD-1 says. |

- Not a target itself: the group takes no press and is not a focus stop (RGP-6), so minimumTarget holds through its radio buttons, each of which keeps a whole touch target of its own at the distance the group puts between them (RAD-9, RAD-21, RGP-2).
- The radio group is a field group that owns a value: its frame is the field group itself, in its radio-group kind (RGP-1, FGR-25), and it adds what a frame cannot have — one value, one change, the arrow keys and the one tab stop kept for the set, and a place in a form as one control. It has no tokens of its own. One level down is the same field group written by hand around radio buttons of one name: the same picture and the same radio group, without the value. A set of checkboxes that is one answer is a checkbox group, which holds a list of values where this holds one; a set of switches, and checkboxes that each have a name of their own, need none of that and stay a field group.
- Selection follows focus: an arrow key selects as it moves, as every platform's own radio buttons do, so the arrow keys never leave focus on an option that is not the answer. A product in which a selection has an expensive or a visible effect at once (it saves, it loads) should take the answer from a button that confirms it, not from change.
- A person cannot return a radio group to "nothing selected". Where no answer is a real answer, add an option that says so ("None", "No preference"), or let the form start with one option selected. The product can still set value to the empty string.
- A group in error is told by its error text, which is words (WCAG 3.3.1), and by the invalid state; the error color its radio buttons take (RAD-26) is one more sign and never the only one (WCAG 1.4.1), and no radio button is exposed as invalid by itself. The error text is announced when it appears under the person's focus and is otherwise read with the group's description (RGP-8, RGP-10); it never takes focus away.
- When a radio group, and when something else. One value from a few options that are all worth seeing at once, in a form or a setting: a radio group, of up to about five options. A choice that changes the content shown below it — which view, which sample, which period: tabs. One value from many options, or where there is little room: a select. Several of several: a checkbox group. On or off, taking effect at once: a switch. Buttons side by side that each toggle on their own: a button group. A segmented button exists only in a design system that ships one.
- Prefer a column. A row is for two or three short options; once it wraps, which label belongs to which ring is harder to see.
- On the web the radio group that assistive technology is told of is the field group's own element inside the component's shadow root, which carries the radiogroup role in its radio-group kind, around the slot that holds the radio buttons; the role's required and invalid states are why a set of radio buttons is not a plain group. iOS has no radio group trait: the container is an accessibility container whose label is the group's, and each option says its position through its value, as the radio button's spec notes.
- Heights are the texts' and the options': at 200% text size every text wraps, a row becomes a column, and nothing is cut (WCAG 1.4.4, 1.4.10).
- How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.
- A radio button the product disabled by itself keeps its place in the count, is passed over by the arrow keys and Tab, and stays selected while value is its value; its value is still submitted, because the value is the group's (RGP-7, RGP-9).
- Read-only (RGP-20) is a set that shows its answer in place and keeps it in the focus order, so that it is read with its form — on a review step, or where only some people may change it: the group says read-only (aria-readonly on the element with role radiogroup on the web, which WAI-ARIA allows on radiogroup and not on radio), each option says whether it is the chosen one, and the arrow keys move among them without choosing, as React Aria's isReadOnly radio group does. Where the answer needs no control at all, write it as text; where no one may reach the set, it is disabled.

### Behaviour

Requirements in EARS form, each with its stable id.

- **RGP-1** THE radio group SHALL be a field group whose kind is radio-group and whose spacing is controls, around its radio buttons: the field group itself, with the field group's own tokens, measures and sentences, and no copy of it — its rows, their order and their gaps (FGR-1), the label above the options (FGR-2), the requirement and its mark (FGR-4, FGR-5, FGR-6), the description under the label, which stays while the group is in error (FGR-7), the error text below the last option (FGR-8) and the disabled frame (FGR-14), each from the prop of the same name, which is the field group's. It SHALL have no look and no token of its own: it SHALL draw no container, edge, fill or elevation, SHALL add no padding around its rows and no distance between its radio buttons but the field group's, and SHALL leave each radio button drawn from the radio button's own tokens, at the radio button's own size.
- **RGP-2** THE radio group SHALL lay out its radio buttons as the field group lays out its controls: WHILE orientation is vertical in one column in the order given (FGR-3), and WHILE orientation is horizontal in a row from the start edge that wraps onto further lines and never cuts, scrolls or narrows an option below what its content needs (FGR-13). The order given is the order of reading, of the arrow keys (RGP-5) and of each option's position (RGP-10) in both orientations.
- **RGP-3** THE radio group SHALL hold one value and show as selected the radio button whose value equals it, and no other; WHILE value is the empty string none SHALL be selected. WHEN the product sets value THE radio group SHALL select the radio button of that value and unselect the others (RAD-3), over the radio button's own selection motion (RAD-15), without emitting change and without moving focus. WHEN the product sets checked on one of the group's radio buttons THE radio group SHALL take that radio button's value as its value, likewise without emitting change, so that the two never disagree; WHEN the product takes checked off the radio button that value selects THE radio group SHALL keep value and show that radio button selected again at once, because the empty string is the one way to say that none is selected. A radio button that comes to the group checked — with the group or after it — SHALL give the group its value only WHILE value is the empty string, the last of several such; WHILE value is not empty, value SHALL decide and the mark SHALL be overruled. IF value equals no radio button's value THEN THE radio group SHALL show none selected, SHALL keep value as it was given, and in debug builds SHALL report it; IF two radio buttons carry one value THEN the first in the order given SHALL be the one value selects; IF a radio button carries the empty string THEN it SHALL never be shown selected, and a person's activation of it SHALL change nothing and emit nothing, because its selection would say that none is selected; in debug builds THE radio group SHALL report either.
- **RGP-4** WHEN the person selects a radio button of the group that was not selected — by pointer, touch, Space, a click on its label, an arrow key (RGP-5) or assistive technology, as RAD-1 says of the radio button — THE radio group SHALL set value to that radio button's value and emit change exactly once, carrying the new value, with value already set when the event is received; the radio button that was selected before SHALL become unselected and emit nothing (RAD-3), and nothing the selected radio button emits SHALL reach a listener on the group as a second change. WHEN the person activates the radio button that is already selected THE radio group SHALL keep value and emit nothing (RAD-2): a person cannot clear a radio group, and a question that may go unanswered has an option that says so.
- **RGP-5** WHEN one of the group's radio buttons has keyboard focus and the person presses Down Arrow or Right Arrow THE radio group SHALL move focus to the next enabled radio button in the order given, wrapping from the last to the first, and select it as RGP-4 says; Up Arrow and Left Arrow SHALL do the same towards the previous one, wrapping from the first to the last; in a right-to-left layout Right Arrow SHALL move to the previous and Left Arrow to the next, and Up Arrow and Down Arrow SHALL stay as they are. The four keys SHALL do this in both orientations, and by the order given, not by the place a wrapped row gave an option. THE radio group SHALL skip a disabled radio button, SHALL do nothing WHILE the focused one is the only enabled one, and SHALL leave a key pressed with Alt, Control or Meta to the platform. This is RAD-4, kept by the group for its radio buttons.
- **RGP-6** THE radio group SHALL keep exactly one of its radio buttons in the focus order: the selected one WHILE it is enabled, otherwise the first enabled one in the order given; Tab and Shift+Tab SHALL enter the group at that radio button from either side and SHALL leave the group from whichever radio button has focus, never moving within it (RAD-5). THE radio group SHALL NOT be a stop itself, and a press on its label, its requirement mark, its description or its error text SHALL select nothing and move no focus into the group (FGR-12). WHEN the radio button that is the stop changes while focus is inside the group THE radio group SHALL leave focus where it is. WHILE no radio button is enabled the group SHALL have no stop. WHEN the product asks the group itself for focus, where the platform has such a call, THE radio group SHALL give focus to the radio button that is the stop.
- **RGP-7** WHILE disabled is true THE radio group SHALL draw its frame from the field group's disabled tokens (FGR-14), SHALL make every radio button it holds disabled as the radio button's own disabled does (RAD-6, RAD-10) — not focusable, not operable, drawn disabled, the selected one still shown selected — SHALL emit no change, SHALL keep value, and SHALL be exposed as disabled, its texts still readable by assistive technology. WHILE disabled is false THE radio group SHALL NOT enable a radio button the product disabled itself: that one stays out of the focus order and of the arrow keys (RGP-5, RGP-6) and is still shown selected WHILE value is its value.
- **RGP-8** THE radio group SHALL take from required and optional what the form asks of the set, as a field group does (FGR-4), and SHALL NOT set the error condition itself because an answer is missing or for any other reason: error and errorText are the product's to set. WHILE error is true THE radio group SHALL draw errorText and the error parts of its frame (FGR-8), SHALL be exposed as invalid (RGP-10), and SHALL have every radio button it holds, selected or not, drawn in the radio button's own error form (RAD-26), as a field group in error marks each control that has an error condition (FGR-8): the refusal is the set's, said by the words below it and by the group's invalid state, and shown on every option, because any of them may be the answer that puts it right. A disabled radio button SHALL be drawn as disabled, without the error color (RAD-26), and THE radio group SHALL NOT expose a radio button as invalid or describe it by errorText: the group says both once (RGP-10). WHEN error becomes true, or errorText changes while error is true, and focus is on one of the group's radio buttons THE radio group SHALL have assistive technology announce errorText once, politely, and SHALL NOT move focus (FGR-9). A change of value SHALL NOT clear error.
- **RGP-9** WHERE the platform has forms that submit named values (the web) THE radio group SHALL take part in its form as one control: it SHALL submit value under its own name WHILE value is not the empty string and the group is not disabled, and nothing otherwise — also WHILE the radio button of that value is one the product disabled by itself, because the value is the group's and a radio button's disabled says who may change it; SHALL return to the value it was first given WHEN the form is reset, without emitting change; SHALL be disabled WHILE the form disables the set it is in; SHALL report a missing value to the form's own validation WHILE required is true and value is the empty string, in the platform's own words for an unanswered set of radio buttons where it has them, and otherwise in errorText, else requiredText, else label — never in words of Tanrei's; and its radio buttons SHALL submit nothing of their own. Enter on a focused radio button SHALL be left to the form (RAD-1). WHERE the platform has no such forms value and change are the whole of it.
- **RGP-10** THE radio group SHALL be exposed to assistive technology as the field group it is, in its radio-group kind, and SHALL add no group to it (FGR-25): one radio group that contains its radio buttons — named by label, whether or not the label is drawn; required WHILE required is true; invalid WHILE error is true; disabled WHILE disabled is true; read-only WHILE readOnly is true (RGP-20); saying its orientation where the platform's radio group has one; and described by, in this order, supportingText, errorText WHILE error is true, and optionalText WHILE optional is true, each new text exposed at once WHEN it changes (FGR-11). requiredText SHALL NOT be part of the description, because the required state carries it, and the requirement tag and the asterisk SHALL be kept from assistive technology as drawn parts. Each radio button SHALL expose its role, its checked state, its position in the group and the group's size as RAD-16 says, counted in the order given with disabled options included. WHERE the platform forms a radio group with a container of its own THE radio group SHALL be that container and SHALL NOT expose a second group around it. WHILE label is empty THE radio group SHALL still render and in debug builds report that the set has no accessible name (FGR-10).
- **RGP-11** WHEN a radio button is added to the group, removed from it or moved, or its value or its disabled changes, while the group is shown THE radio group SHALL lay out its radio buttons again at once, without a motion; SHALL select by value again (RGP-3), so that a radio button that arrives with the group's value is shown selected and value is kept WHEN the selected one leaves; SHALL give the disabled state it holds (RGP-7) to one that came and take it back from one that left; SHALL set the tab stop (RGP-6) and each position (RGP-10) again; SHALL emit no change; and SHALL leave focus on the radio button that has it.
- **RGP-12** THE radio group SHALL hold radio buttons and nothing else that can be operated. IF it holds no radio button THEN THE radio group SHALL draw its rows without one and in debug builds report it; IF it holds another control, or a radio button that names a group of its own (RAD-25), THEN in debug builds THE radio group SHALL report it. THE radio group SHALL NOT restyle a radio button or change how it answers hover, focus or a press: those stay the radio button's (RAD-7, RAD-8, RAD-11 to RAD-13).
- **RGP-13** WHILE the text size is enlarged THE radio group SHALL grow with its texts as a field group does (FGR-16): the label, the description, the error text and each option's label wrap and never clip, the requirement tag grows with its text, a horizontal row wraps and at 200% becomes a column where one line no longer holds two options (RGP-2), and no text SHALL be lost, lie over another part or need scrolling in two directions.
- **RGP-14** WHERE the platform draws with a person's own forced colors (the web's forced-colors mode, and a platform's equivalent where it has one) THE radio group SHALL draw its frame as a field group does there (FGR-17) — its texts in the platform's text color, its requirement tag with an edge — and SHALL rely on no color of its own to show an error: the error is carried by the words of errorText and by the invalid state (RGP-10), and its radio buttons are drawn there as out of error (RAD-27).
- **RGP-15** WHERE the layout direction is right-to-left THE radio group SHALL mirror its layout as a field group does (FGR-18): the label row, the description, the options and the error text start at the right edge, the requirement mark stands at the left of the label, and a horizontal row runs from right to left, the first option at the right; a column keeps its order from top to bottom. Each radio button mirrors itself (RAD-17), and Left Arrow and Right Arrow exchange what they do (RGP-5).
- **RGP-16** THE radio group SHALL take its layout direction from where it is used and SHALL have no direction setting of its own, as the field group (FGR-19) and each radio button (RAD-22) do; WHEN that direction changes while the radio group is shown THE radio group SHALL be drawn for the new direction at once, without a motion of its own, keeping its value, its selection and its focus, and the arrow keys SHALL follow the new direction at the next key (RGP-5).
- **RGP-17** THE radio group SHALL lay out its label, its description, its error text and the text of its requirement tag as the field group does (FGR-21): each as text of its own direction, isolated from the text around it, and placed and aligned by the layout direction, not by the text's own. Each option's label is the radio button's (RAD-23).
- **RGP-18** WHEN requiredMark or orientation names a value the radio group does not know THE radio group SHALL do with it what the field group does (FGR-22) — render with the prop's default (auto, vertical) — and in debug builds the unknown value SHALL be reported once.
- **RGP-19** THE radio group SHALL have no words of its own: requiredText and optionalText are the field group's, read as FGR-24 says — from the instance's prop and, WHILE that is empty, from the product's words of the same name, and from nowhere else. IF neither gives requiredText WHILE required is true and the required mark is tag THEN THE radio group SHALL show the asterisk of FGR-6 in the tag's place, as the field group does, and SHALL keep its required state (RGP-10), which needs no word. IF neither gives optionalText WHILE optional is true THEN THE radio group SHALL show no tag and SHALL add nothing to the description for it. In debug builds the missing words SHALL be reported once.
- **RGP-20** WHILE readOnly is true and disabled is false THE radio group SHALL make every radio button it holds read-only as the radio button's own readOnly does (RAD-28), without changing the readOnly the product gave any of them: the person can enter the group at its stop, which stays the selected radio button (RGP-6), move focus among its enabled radio buttons with the arrow keys as RGP-5 says while selecting none, and read each one's label, checked state and position, and cannot change value by any input. THE radio group SHALL then emit no change, SHALL keep value, SHALL be exposed as read-only beside its other states (RGP-10), SHALL be submitted with its form as RGP-9 says, SHALL NOT report a missing value to the form's own validation, because the person cannot give one, and SHALL draw its frame and its radio buttons from their enabled tokens, never the disabled ones. WHILE disabled is true as well, disabled wins (RGP-7).
- **RGP-21** WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE radio group SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on the radio group of the field group it is (RGP-10) — an id reference by the elements it names in the host's tree or a tree around it, a description after its own — except those the radio group owns (aria-label, aria-labelledby, aria-disabled, aria-required, aria-invalid, aria-errormessage, aria-readonly), which it SHALL move and not expose: its name is label and its states are its props (RGP-10, RGP-20). WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE radio group SHALL take it off that element. WHEN the product sets an aria- attribute the radio group owns THE radio group SHALL report it in debug builds.

### Tokens

As written in the token documents (DTCG): an alias in braces names the token it takes its value from, which each design system resolves. On the web each token is a CSS custom property: `--tanrei` and the name after `tanrei`, joined by hyphens (`tanrei.comp.field-group.…` → `--tanrei-comp-field-group-…`).

#### `tanrei.comp.field-group`

| Token | Type | Value |
| --- | --- | --- |
| `comp.field-group.label.typescale` | `typography` | `{tanrei.sys.typescale.label}` |
| `comp.field-group.label.gap` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.field-group.requirement.mark` | `number` | `{tanrei.sys.behavior.requirement-mark}` |
| `comp.field-group.requirement.gap` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.field-group.requirement.asterisk-gap` | `dimension` | `{tanrei.sys.space.space25}` |
| `comp.field-group.supporting-text.typescale` | `typography` | `{tanrei.sys.typescale.label-small}` |
| `comp.field-group.supporting-text.gap` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.field-group.error-text.gap` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.field-group.gap.controls` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.field-group.gap.fields` | `dimension` | `{tanrei.sys.space.space300}` |
| `comp.field-group.gap-inline` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.field-group.field.min-width` | `dimension` | `144px` |
| `comp.field-group.standard.label.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.field-group.standard.supporting-text.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.field-group.standard.required-asterisk.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.field-group.standard.error-text.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.field-group.standard.error.label.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.field-group.standard.error.supporting-text.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.field-group.standard.error.required-asterisk.color` | `color` | `{tanrei.sys.color.primary}` |

Per-state tokens, valued by the design system's pattern rules (not written by hand):

- `comp.field-group.<variant>.<state>.<property>` for the variants `standard`, the states `disabled` and the properties `label.color`, `supporting-text.color`, `required-asterisk.color`, `error-text.color`, `error.label.color`, `error.supporting-text.color`, `error.required-asterisk.color`.

The radio group has no tokens of its own. The table above lists the field group’s, which draws its frame: a design system that restyles the field group restyles every radio group with it. Each radio button is drawn from the radio button’s own tokens.

### What each member gives

| What is drawn | Whose |
| --- | --- |
| The label, the requirement mark, the description, the error text, the distances between the rows and between the radio buttons | The [field group](https://tanrei.chnotchy.com/components/field-group/)’s, from `comp.field-group.*` |
| Each radio button: the ring, the fill, the label, the state layer, the focus ring, the disabled form | The [radio button](https://tanrei.chnotchy.com/components/radio-button/)’s, from `comp.radio-button.*` |
| Anything else | Nothing: the radio group draws nothing |

### Setting one group apart

A design system styles the frame once, for every field group, radio group and checkbox group. One instance is set apart on the instance: on the web, a field group token set on the radio group reaches the frame inside it.

```html
<tanrei-radio-group label="Delivery"
  style="--tanrei-comp-field-group-gap-controls: 12px">…</tanrei-radio-group>
```

### What the web draws differently

- The element’s shadow root holds a `<tanrei-field-group kind="radio-group">` around a slot. The radio buttons stay your own children. Importing the element defines both members.
- The element is form-associated. It takes `name` and `form`, and submits `value` under `name` — also while the selected radio button is one you disabled by itself. Its radio buttons submit nothing, whatever `name` they carry. It resets to its `value` attribute, follows a disabled `<fieldset>`, and reports a missing value while `required` is set and `value` is empty, in the browser’s own words for an unanswered set of radio buttons.
- `change` is a CustomEvent whose `detail.value` is the new value; `input` is fired before it. A listener on a radio button inside the group hears neither.
- Parts: `field-group` is the frame, and its parts have the same names here as one level down: `label`, `requirement-label`, `supporting-text`, `error-text`. Your own radio buttons are styled as they are anywhere, with `tanrei-radio-button::part(…)`.
- The element is not focusable itself. `focus()` goes to the radio button that is the stop, and a page’s own `<label for>` moves focus there.
- The element has no role: the radio group is the field group inside it. Name it with `label` — with `label-hidden` where the label is drawn elsewhere — and never with `aria-label`, which names nothing here.
- The attributes are kebab-case: `label-hidden`, `supporting-text`, `error-text`, `required-mark`, `required-text`, `optional-text`.
- Before the element is defined, and without script, nothing of its own is drawn: the radio buttons show as plain content. A page that must work without script writes a native `<fieldset>` of native radio buttons of one `name`, which submits the same entry.

## Guidelines

### When a radio group, and when something else

| The person | Use |
| --- | --- |
| Gives one value from a few options that are all worth seeing at once — up to about five — in a form or a setting | A radio group. |
| Changes what is shown below: which view, which sample, which period | [Tabs](https://tanrei.chnotchy.com/components/tabs/). |
| Gives one value from many options, or there is little room | A [select](https://tanrei.chnotchy.com/components/select/). |
| Gives several values as one answer | A [checkbox group](https://tanrei.chnotchy.com/components/checkbox-group/). |
| Writes the same set by hand, to arrange it as the group does not | A [field group](https://tanrei.chnotchy.com/components/field-group/) whose `kind` is `radio-group`, around radio buttons of one name ([one level down](https://tanrei.chnotchy.com/components/radio-group/#one-level-down)). |
| Turns one thing on or off, at once | A [switch](https://tanrei.chnotchy.com/components/switch/). |
| Presses buttons that stand side by side and each toggle on their own | A [button group](https://tanrei.chnotchy.com/components/button-group/). |

The test between a radio group and tabs is what the choice does, not how it looks: a value that is stored or submitted is a radio group; a choice of what to look at is tabs.

### The value

- `value` is the value of the selected radio button, and the empty string while none is selected. Set it to select; listen to `change` to hear a person’s selection.
- `change` is emitted once per selection, by the group, with the value the group already holds. It is not emitted when you set `value`, or when the selected option is activated again.
- A person cannot return a radio group to “nothing selected”. Where no answer is a real answer, add an option that says so (“None”, “No preference”), or start with one option selected.
- Selection follows focus: an arrow key selects as it moves. If a selection has an expensive or a visible effect at once, take the answer from a button that confirms it, not from `change`.
- A value that no radio button has shows none selected and is kept: the option may arrive later.

### Layout

- `orientation="vertical"` (the default) stacks the options in one column. Prefer it.
- `orientation="horizontal"` lays them out in a row that wraps. Keep it for two or three short options: once a row wraps, which label belongs to which ring is harder to see.

The arrow keys are the same in both, and go by the order given, not by where a wrapped row put an option.

### Errors

`error` and `errorText` are yours to set: the group never puts itself in error, not even when it is required and unanswered. A group in error draws the error text below the options, is exposed as invalid, and draws every radio button in its error form: the ring and the dot take the error color, as the boxes of a checkbox group do. A disabled option shows no error color, because it cannot be the answer that puts the error right. The radio buttons themselves are not exposed as invalid; the group says it once.

A change of value does not clear the error. Clear it when the answer is acceptable.

### Read-only

A read-only group shows its answer in place, for the person to read and not change: a review step, or a choice that only some people may change. Tab still enters it at the selected radio button, the arrow keys move among the options without selecting any, and the value is submitted with the form. A required, read-only group left unanswered is not refused by the form, because the person cannot answer it. It is drawn as an enabled group, without the hover and press feedback.

### Do

- Name the set with the question its options answer: “Delivery”.
- Give each radio button a `value` that is unique in the group, and its own label.
- Keep the options few. One option is a checkbox; many are a select.

### Don’t

- Don’t put anything else that can be operated in a radio group: no checkbox, no button, no field.
- Don’t give the radio buttons of a radio group a `name`. It is ignored: the group is their group.
- Don’t wrap a radio group in a field or a field group to give it a label. It has its own. A radio group may be one member of a field group that names a larger set (“Shipping”).
- Don’t make a set read-only by disabling the group: a disabled group submits nothing and cannot be reached. Use `readOnly`, which keeps the set in the focus order and submits its value.

## Accessibility

### What the component handles

- One radio group that contains its radio buttons, named by `label`, drawn or hidden with `labelHidden`.
- The required state while `required` is set, the invalid state while `error` is true, the disabled state while `disabled` is true, and its orientation.
- The read-only state while `readOnly` is true. Every radio button is then read-only: Tab and the arrow keys still move focus, and nothing a person does changes `value`.
- The group’s description is `supportingText`, then `errorText` while `error` is true, then `optionalText` while it is optional. `requiredText` is not part of it: the required state says it.
- Each radio button says its position in the set and the set’s size, disabled options counted.
- One stop in the focus order: the selected radio button, or the first enabled one. Tab enters the group there and leaves it; the arrow keys move within it, skip a disabled option and wrap at both ends. In a right-to-left layout Left Arrow and Right Arrow exchange.
- `disabled` disables every radio button. An enabled group never enables a radio button you disabled yourself: that one is passed over by the arrow keys and Tab, keeps its place in the count, and stays selected while `value` is its value.
- An error that appears, or changes its words, while focus is on one of the radio buttons is announced once, politely. Focus never moves because of an error.
- The requirement tag and the asterisk are hidden from assistive technology.
- At 200% text every text wraps, a row becomes a column, and nothing is cut.
- In forced colors every text takes the system’s text color and the requirement tag gains an edge.
- In a right-to-left layout the rows start at the right, a row of options runs from the right, and each text keeps its own direction.
- On the web it is one control of its form: it submits `value` under `name`, returns to its first value on reset, follows a disabled `fieldset`, and reports a missing value to the form’s own validation while it is required and unanswered.
- A group with no radio button, with another control, with no label, with two options of one value, or with a value no option has, is reported in debug builds.

Known gaps on the web:

- Before the element is defined, the radio buttons are on the page without the group’s label, description, layout and selection.

### What you need to do

- Give the group a `label`.
- Validate the answer yourself, set `error`, and write `errorText` so that it says how to put the answer right.
- A required group left unanswered is refused by the form’s own validation in the browser’s words, as a select is. To say it in your product’s words instead, do as GOV.UK does: put `novalidate` on the form, and when it is submitted set `error` and `errorText` on the group.
- Give the words of the requirement: `required-text` and `optional-text` on the group, or once for the whole product with `setWords({ requiredText, optionalText })`. Tanrei ships none: without `requiredText` an asterisk stands in the tag’s place, and without `optionalText` no tag is drawn and nothing is said. See [The product’s words](https://tanrei.chnotchy.com/foundations/words/).
- After a failed submission, also say above the form what was refused and link to the group: a live [callout](https://tanrei.chnotchy.com/components/callout/) in its error status.
