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.
- Category
- Selection
- Tokens
comp.field-group- Level
- Composed
- Built from
- Field group, Radio button
- Web
tanrei-radio-groupschedule - SwiftUI
TanreiRadioGroupradio_button_unchecked - Jetpack Compose
TanreiRadioGroupradio_button_unchecked - Flutter
TanreiRadioGroupradio_button_unchecked - React Native
RadioGroupradio_button_unchecked
Generated from docs/status/status.json, specs/radio-group.spec.json
A set of radio buttons with a name, a description and a requirement
A 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 and radio buttons. 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.
<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>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.
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:
<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>The same set written by hand: a field group whose kind is radio-group, with radio buttons of one name in it
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.
An unanswered required set in error, a refused answer, a row, and a set with one option disabled
Anatomy
| Part | Description |
|---|---|
1 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. |
2 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. |
Properties
| Property | Type | Default | Description |
|---|---|---|---|
value | string | "" | 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 | "" | Names the set, visibly and to assistive technology: the question its controls answer ("Notify me by"). Required. — Field group |
labelHidden | boolean | false | Does not draw the label, which stays the accessible name of the group. For a set whose visible heading is elsewhere. — Field group |
supportingText | string | "" | The description: how to answer ("Choose all that apply"), drawn under the label and given to assistive technology as the group's description. — Field group |
errorText | string | "" | 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. — 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 | 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. — 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 | Disables every control of the set and draws the frame as disabled. — Field group The value is kept, and on the web it is not submitted (RGP-7, RGP-9). |
readOnly | boolean | false | 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 | 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. — 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 | 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. — Field group |
requiredMark | auto | tag | asterisk | none | "auto" | 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. — Field group |
requiredText | string | "" | 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. — Field group Not part of the radio group's description: its required state carries it (RGP-10). |
optionalText | string | "" | 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. — Field group |
orientation | vertical | horizontal | "vertical" | 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. — 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. |
Items
The kinds of item the component repeats. A fixture lists its items in order; each platform names each kind in its own idiom.
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.
| Property | Type | Default | Description |
|---|---|---|---|
value | string | "" | 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 | "" | Its own label, as the Radio button's label. |
disabled | boolean | false | 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. |
| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| Item | tanrei-radio-button | TanreiRadioButton | TanreiRadioButton | TanreiRadioButton | RadioButton |
value · Property | value | value | value | value | value |
label · Property | label | title | label | label | label |
disabled · Property | disabled | disabled | enabled | enabled | disabled |
| Notes | A tanrei-radio-button child without a name: inside a tanrei-radio-group it takes its group, its checked, its tab stop and its position from the group and submits nothing itself (RAD-25). |
States
Behavior
Requirements in EARS form. Every platform implementation is tested against these ids.
| ID | Requirement |
|---|---|
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. |
Platform name mapping
Parity does not mean identical APIs. Each platform uses its own idiom for the same contract.
| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| Component | tanrei-radio-group | TanreiRadioGroup | TanreiRadioGroup | TanreiRadioGroup | RadioGroup |
value · Property | value | selection | value | groupValue | value |
label · Property | label | label | label | labelText | label |
labelHidden · Property | labelHidden | labelsHidden | labelHidden | labelHidden | labelHidden |
supportingText · Property | supportingText | supportingText | supportingText | helperText | supportingText |
errorText · Property | errorText | errorText | errorText | errorText | errorText |
error · Property | error | error | isError | isError | error |
disabled · Property | disabled | disabled | enabled | enabled | disabled |
readOnly · Property | readOnly | readOnly | readOnly | readOnly | readOnly |
required · Property | required | isRequired | required | required | required |
optional · Property | optional | isOptional | optional | optional | optional |
requiredMark · Property | requiredMark | requiredMark | requiredMark | requiredMark | requiredMark |
requiredText · Property | requiredText | requiredText | requiredText | requiredText | requiredText |
optionalText · Property | optionalText | optionalText | optionalText | optionalText | optionalText |
orientation · Property | orientation | orientation | orientation | orientation | orientation |
change · Event | change | selection | onValueChange | onChanged | onValueChange |
| Notes | <tanrei-radio-group label="Delivery" name="delivery" value="express"><tanrei-radio-button value="standard" label="Standard"></tanrei-radio-button>…</tanrei-radio-group>; class TanreiRadioGroup; in @tanrei/react, RadioGroup with value and onChange. Composed in the construction the checkbox group has: the members that hold the value are the product's own children. The shadow root holds one <tanrei-field-group part="field-group" kind="radio-group" spacing="controls"> — the member's own element, not drawing code shared with it — around a <slot>; the radio buttons stay in the product's light DOM and are assigned through the group's slot into the field group's. The eleven props that are the field group's are set on that element as they are given, and its documented parts — label, requirement-label, supporting-text, error-text — are exported under its own names (exportparts); the group writes none of its custom properties, so --tanrei-comp-field-group-* set on the host is inherited by the field group inside. The radio group that assistive technology is told of is the field group's own element in its radio-group kind — name, description, required, invalid, disabled and orientation, once (FGR-25): the host has no role, and nothing is tied across a shadow root. The field group gives disabled to each radio button by the tanrei-field-connect event it already answers — a slotted radio button's event passes the field group on its composed path before it reaches the host — and writes the announcement of RGP-8. The host takes disabled from outside by itself — from a field group around it (FGR-15) and from <fieldset disabled> — and gives the field group inside the three together; the inner field group's own question to a group around it is stopped at the host, so that an outer field group counts one member, the radio group (a frame's question, by contrast, leaves). What the host keeps is what a frame cannot do. A form-associated custom element (static formAssociated = true): name is a web attribute, not a spec prop; the value attribute is the first value and what a reset returns to, the value property is the live one; it submits value, restores it on navigation (formStateRestoreCallback), follows <fieldset disabled> and reports valueMissing while required and value is empty, anchored on the radio button that is the tab stop, with the browser's own message for an unanswered set of radio buttons, in the browser's language — a required native radio button is asked once — and, where a browser gives none, errorText, else the required text, else label. A radio button finds its group with closest('tanrei-radio-group') when it connects; the group's radio buttons are those among its descendants whose nearest radio group it is — direct children or deeper, inside the product's own elements — found again when its child list changes (a MutationObserver: slotchange does not fire for a change below a slotted element); a radio button inside a shadow root of the product's own is found by neither side and groups by its name there. The group sets each option's checked, and its radio buttons take their tab stop, aria-posinset and aria-setsize from it again whenever its options change (RGP-11); it handles the arrow keys on the host. A radio button's own disabled attribute is never written, so an enabled group cannot enable what the product disabled. A radio button inside a group fires no input and no change at all — stopped at the group they would still pass a capture listener on an ancestor — it tells its group, which sets value and fires input and one change, a bubbling CustomEvent whose detail.value is the value; a listener set on a radio button inside a group hears nothing. A press on the frame's texts selects nothing and moves no focus into the group (RGP-6); the browser's own answer to a press on plain text — focus leaves what had it — is not prevented, because preventing it would prevent selecting the text. The host is not focusable and does not delegate focus, because its controls are light-DOM children; focus() moves to the radio button that is the stop. Radio buttons with one name and no tanrei-radio-group still form a group by themselves (RAD-25); inside a <tanrei-field-group kind="radio-group"> they are this component one level down. Attributes are kebab-case: label-hidden, supporting-text, error-text, required-mark, required-text, optional-text. The host is display: block. Without script the element is not defined and draws nothing of its own, as every Tanrei element with a shadow root; a page that must work without script writes a native fieldset of native radio buttons of one name, which submits the same entry. Words: the element has no words of its own; required-text and optional-text left empty read the product's words through the field group. Its field group gives each radio button the error condition, and the radio button draws its error form (RGP-8, FGR-8, RAD-26). readOnly is the readonly attribute: each radio button it holds is read-only while it is (their own readonly unchanged), the arrow keys move focus without selecting, and aria-readonly is written on the field group's radiogroup element, beside the other states it carries. | TanreiRadioGroup("Delivery", selection: $delivery, supportingText:, errorText:, error:, required:, optional:, orientation:) { TanreiRadioButton("Standard", value: Delivery.standard) … }, generic over a Hashable value: selection is a Binding<V?>, nil for none, and writing it is the change event. This is the container the radio button already uses to form a group on this platform (its name), which so far drew nothing; it gains the frame, which is the platform's field group itself in its radio-group kind (FGR-25), and the radio buttons inside read the selection from it. labelHidden follows .labelsHidden() and disabled follows .disabled(_:). The options are laid out by the field group's Layout (a column, or a row that wraps). iOS has no radio group trait: the container is .accessibilityElement(children: .contain) with the label and the description as its label and hint, and required and invalid are added to the hint in words. No forms: RGP-9 does not apply. The product's words are read from the \.tanreiWords environment value. Not built on this platform yet: in error every option is drawn in its error form (RGP-8, RAD-26). | TanreiRadioGroup(label = "Delivery", value = delivery, onValueChange = { delivery = it }, supportingText = …, isError = …, required = …, optional = …, orientation = …) { TanreiRadioButton(value = "standard", label = "Standard") … }: controlled, as Compose is — the group calls onValueChange and the caller moves value. The content runs in a TanreiRadioGroupScope whose TanreiRadioButton(value, label) takes selected and its click from the group; the group is built around TanreiFieldGroup in its radio-group kind (FGR-25), which applies Modifier.selectableGroup() itself, so the product does not, and has the name, the description and the error in its semantics. enabled is the inverse flag. A Column or a FlowRow. The bare TanreiRadioButton(selected, onClick) inside a selectableGroup() stays for options that are not laid out by a group (RAD-25). No forms: RGP-9 does not apply. The product's words are read from LocalTanreiWords. Not built on this platform yet: in error every option is drawn in its error form (RGP-8, RAD-26). | TanreiRadioGroup<T>(groupValue:, onChanged:, labelText: 'Delivery', helperText: …, errorText: …, isError: …, required: …, optional: …, orientation: …, children: [TanreiRadioButton<T>(value:, label:), …]). This is the TanreiRadioGroup<T> the radio button already stands in on this platform, which so far drew nothing and took a semanticLabel; it gains the frame, which is the platform's field group itself in its radio-group kind (FGR-25), and labelText with labelHidden replaces semanticLabel. groupValue is nullable for none; enabled is the inverse flag. A Column or a Wrap; the radio-group semantics, the arrow keys and the single tab stop stay where they are. FormField integration is the product's: RGP-9 does not apply. The product's words are read from the TanreiWords inherited widget, which the app fills from its own localizations. Not built on this platform yet: in error every option is drawn in its error form (RGP-8, RAD-26). | <RadioGroup label="Delivery" value={delivery} onValueChange={setDelivery} supportingText="…"><RadioButton value="standard" label="Standard" />…</RadioGroup>: controlled. This is the RadioGroup parent the radio button already uses on this platform, which so far drew nothing; it gains the frame, which is the platform's field group itself in its radio-group kind (FGR-25), and takes selected and onSelect off its children: an option inside reads both from the group's context. accessibilityRole="radiogroup" with accessibilityLabel and accessibilityHint; required and invalid are said in the hint, because React Native has neither state for a group. A column, or a row with flexWrap. Follows the app's direction where an ancestor's is not known. No forms: RGP-9 does not apply. The product's words are read from the TanreiWordsProvider context. Not built on this platform yet: in error every option is drawn in its error form (RGP-8, RAD-26). |
Tokens
| Token | Aliases | Resolved (light) |
|---|---|---|
--tanrei-comp-field-group-error-text-gap | sys.space.space50 | 4px |
--tanrei-comp-field-group-field-min-width | — | 144px |
--tanrei-comp-field-group-gap-inline | sys.space.space200 | 16px |
--tanrei-comp-field-group-gap-controls | sys.space.space100 | 8px |
--tanrei-comp-field-group-gap-fields | sys.space.space300 | 24px |
--tanrei-comp-field-group-label-gap | sys.space.space100 | 8px |
--tanrei-comp-field-group-requirement-asterisk-gap | sys.space.space25 | 2px |
--tanrei-comp-field-group-requirement-gap | sys.space.space100 | 8px |
--tanrei-comp-field-group-requirement-mark | sys.behavior.requirement-mark | 0 |
--tanrei-comp-field-group-standard-disabled-error-text-color | — | |
--tanrei-comp-field-group-standard-disabled-error-label-color | — | |
--tanrei-comp-field-group-standard-disabled-error-required-asterisk-color | — | |
--tanrei-comp-field-group-standard-disabled-error-supporting-text-color | — | |
--tanrei-comp-field-group-standard-disabled-label-color | — | |
--tanrei-comp-field-group-standard-disabled-required-asterisk-color | — | |
--tanrei-comp-field-group-standard-disabled-supporting-text-color | — | |
--tanrei-comp-field-group-standard-error-text-color | sys.color.error | |
--tanrei-comp-field-group-standard-error-label-color | sys.color.on-surface | |
--tanrei-comp-field-group-standard-error-required-asterisk-color | sys.color.primary | |
--tanrei-comp-field-group-standard-error-supporting-text-color | sys.color.on-surface-variant | |
--tanrei-comp-field-group-standard-label-color | sys.color.on-surface | |
--tanrei-comp-field-group-standard-required-asterisk-color | sys.color.primary | |
--tanrei-comp-field-group-standard-supporting-text-color | sys.color.on-surface-variant | |
--tanrei-comp-field-group-supporting-text-gap | sys.space.space50 | 4px |
--tanrei-comp-field-group-label-typescale | sys.typescale.label | 500 14px/1.4286 |
--tanrei-comp-field-group-supporting-text-typescale | sys.typescale.label-small | 500 12px/1.3333 |
Fixtures
Canonical renderings. Every platform snapshots each fixture under the same id so the results can be compared.
default
value: standard label: Delivery items: 3 none-selected
label: Delivery items: 3 supporting
value: express label: Delivery supportingText: You can change this until the order ship… items: 3 label-hidden
value: standard label: Delivery labelHidden: true supportingText: You can change this until the order ship… items: 3 required
value: standard label: Delivery supportingText: You can change this until the order ship… required: true requiredText: Required items: 3 optional
label: Delivery optional: true optionalText: Optional items: 3 required-asterisk
value: standard label: Delivery required: true requiredMark: asterisk items: 3 required-error
label: Delivery errorText: Choose how the order reaches you. error: true required: true requiredText: Required items: 3 error-selected
value: express label: Delivery supportingText: You can change this until the order ship… errorText: Express delivery does not reach this add… error: true required: true requiredText: Required items: 3 error-one-disabled
value: express label: Delivery supportingText: You can change this until the order ship… errorText: Express delivery does not reach this add… error: true required: true requiredText: Required items: 3 horizontal
value: m label: Size orientation: horizontal items: 3 horizontal-many
value: fri label: Send the report supportingText: One report, on the day you choose. orientation: horizontal items: 9 disabled
value: standard label: Delivery supportingText: You can change this until the order ship… disabled: true required: true requiredText: Required items: 3 one-disabled
value: standard label: Delivery items: 3 long-label
label: How should we deliver the parts of this… supportingText: You can change this until the first parc… errorText: Choose how the order reaches you, or go… error: true required: true requiredText: Required items: 3 required-ja
value: standard label: 配送方法 supportingText: 発送されるまで変更できます。 required: true requiredText: 必須 items: 3 error-ja
label: 配送方法 errorText: 配送方法を選んでください。 error: true required: true requiredText: 必須 items: 3 rtl
value: express label: طريقة التوصيل supportingText: يمكنك تغيير هذا حتى يُشحن الطلب. required: true requiredText: مطلوب direction: rtl items: 3 horizontal-rtl
label: גודל errorText: יש לבחור גודל. error: true required: true requiredText: חובה orientation: horizontal direction: rtl items: 3 ltr-label-rtl
value: 5 label: Wi-Fi 6E supportingText: 2.4 GHz, 5 GHz, 6 GHz direction: rtl items: 3 Generated from specs/radio-group.spec.json, tokens/comp/field-group.tokens.json, design/tokens/
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 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’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’s, from comp.radio-button.* |
| Anything else | Nothing: the radio group draws nothing |
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.
<tanrei-radio-group label="Delivery"
style="--tanrei-comp-field-group-gap-controls: 12px">…</tanrei-radio-group>- 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
nameandform, and submitsvalueundername— also while the selected radio button is one you disabled by itself. Its radio buttons submit nothing, whatevernamethey carry. It resets to itsvalueattribute, follows a disabled<fieldset>, and reports a missing value whilerequiredis set andvalueis empty, in the browser’s own words for an unanswered set of radio buttons. changeis a CustomEvent whosedetail.valueis the new value;inputis fired before it. A listener on a radio button inside the group hears neither.- Parts:
field-groupis 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, withtanrei-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— withlabel-hiddenwhere the label is drawn elsewhere — and never witharia-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 onename, which submits the same entry.
| 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. |
| Gives one value from many options, or there is little room | A select. |
| Gives several values as one answer | A checkbox group. |
| Writes the same set by hand, to arrange it as the group does not | A field group whose kind is radio-group, around radio buttons of one name (one level down). |
| Turns one thing on or off, at once | A switch. |
| Presses buttons that stand side by side and each toggle on their own | A 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.
valueis the value of the selected radio button, and the empty string while none is selected. Set it to select; listen tochangeto hear a person’s selection.changeis emitted once per selection, by the group, with the value the group already holds. It is not emitted when you setvalue, 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.
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.
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.
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.
- Name the set with the question its options answer: “Delivery”.
- Give each radio button a
valuethat is unique in the group, and its own label. - Keep the options few. One option is a checkbox; many are a select.
- 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.
- 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
- The hit area meets the minimum target size even when the visual size is smaller. minimum 32px · pointer 44px · spacing 8px · touch 48px
Keyboard
| 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. |
Notes
- 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.
Generated from specs/radio-group.spec.json, tokens/sys/measure.tokens.json
- One radio group that contains its radio buttons, named by
label, drawn or hidden withlabelHidden. - The required state while
requiredis set, the invalid state whileerroris true, the disabled state whiledisabledis true, and its orientation. - The read-only state while
readOnlyis true. Every radio button is then read-only: Tab and the arrow keys still move focus, and nothing a person does changesvalue. - The group’s description is
supportingText, thenerrorTextwhileerroris true, thenoptionalTextwhile it is optional.requiredTextis 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.
disableddisables 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 whilevalueis 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
valueundername, returns to its first value on reset, follows a disabledfieldset, 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.
- Give the group a
label. - Validate the answer yourself, set
error, and writeerrorTextso 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
novalidateon the form, and when it is submitted seterroranderrorTexton the group. - Give the words of the requirement:
required-textandoptional-texton the group, or once for the whole product withsetWords({ requiredText, optionalText }). Tanrei ships none: withoutrequiredTextan asterisk stands in the tag’s place, and withoutoptionalTextno tag is drawn and nothing is said. See The product’s words. - After a failed submission, also say above the form what was refused and link to the group: a live callout in its error status.