Radio Button
One choice out of a set. RadioGroup is the APG pattern — a named group, a shared field name, and the arrow keys between the options.
Import
JSX
import RadioGroup from '@box-kite/react/components/radioGroup';
import RadioButton from '@box-kite/react/components/radioButton';A group
Plan
JSX
<RadioGroup label="Plan" name="plan" defaultValue="free">
<RadioGroup.Item value="free" label="Free" />
<RadioGroup.Item value="pro" label="Pro" />
<RadioGroup.Item value="team" label="Team" />
</RadioGroup>What the group is for
A set of radios with nothing over it is, to a screen reader, a handful of unrelated controls.
RadioGroup gives them role="radiogroup" named by its own label, hands every RadioGroup.Item the same name — so the set submits as one field, generated if you do not supply one — and owns the selected value.The tab order it deliberately leaves alone. A native radio set is already a single tab stop, with the checked option holding it; a
tabIndex of ours would only fight the platform. The arrow keys are the half worth owning, and they select as they move, which is what APG asks of a radio group and what distinguishes it from a listbox.Horizontal
Billing
JSX
<RadioGroup label="Billing" orientation="horizontal" defaultValue="monthly">
<RadioGroup.Item value="monthly" label="Monthly" />
<RadioGroup.Item value="yearly" label="Yearly" />
</RadioGroup>Controlled, with the reason it changed
Plan
free · last reason: —JSX
// A radio group can also hold nothing, so the state is string | undefined.
const [plan, setPlan] = useState<string | undefined>('free');
<RadioGroup
label="Plan"
value={plan}
onValueChange={(next, { reason }) => {
setPlan(next);
console.log(reason); // 'click' | 'keyboard'
}}
>
<RadioGroup.Item value="free" label="Free" />
<RadioGroup.Item value="pro" label="Pro" />
</RadioGroup>One radio on its own
JSX
<RadioButton name="plan" value="free" label="Free" defaultChecked />
<RadioButton name="plan" value="pro" label="Pro" />The label is the component's job
label renders the text inside a <label> that wraps the input, so the two are associated with no htmlFor/id pair to keep in sync and the whole row is a click target. Style that element with labelProps; every other Box prop still styles the radio itself.JSX
<RadioButton name="plan" value="pro" label="Pro" labelProps={{ gap: 3, fontSize: 14 }} />Disabled
JSX
<RadioButton name="plan" value="free" label="Free" disabled defaultChecked />
<RadioButton name="plan" value="pro" label="Pro" disabled />Clean
JSX
<RadioButton clean name="plan" value="free" label="Free" defaultChecked />RadioButton props
Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on /box rather than repeated here.
| Prop | Type | Default | What it does |
|---|---|---|---|
name | string | — | The name the radios of one set share — it is what makes them one choice rather than several. |
props | RadioButtonTagProps | — | Attributes for the <input> itself. type is the component's. |
onInput | (e: React.ChangeEvent<HTMLInputElement>) => void | — | Fires on every click or keystroke. On a radio this is the same moment as onChange. |
onChange | (e: React.ChangeEvent<HTMLInputElement>) => void | — | Fires when this radio becomes the chosen one. A radio never fires it on being cleared. |
value | string | number | — | What the set submits when this option is the chosen one. |
autoFocus | boolean | — | Focus it on mount. One per page at most — a stolen focus is a lost place in the document. |
readOnly | boolean | — | Rendered and read, but not selectable. Unlike disabled it stays in the tab order and submits. |
defaultChecked | boolean | — | Makes this the chosen option to begin with, when the set is uncontrolled. |
label | React.ReactNode | — | The text beside the radio, rendered inside a <label> that wraps the input. |
labelProps | BoxProps<'label'> | — | Styles for the wrapping <label>: the row's layout, not the radio's own appearance. |
RadioButton keyboard
| Key | Result |
|---|---|
Tab | Enters the set once, landing on the chosen option — or on the first, when none is chosen. Tab again leaves the set entirely. |
Down / Up, Right / Left | The next or previous radio of the same name, choosing it as focus arrives, wrapping at both ends. The browser supplies this, so it works with no RadioGroup. |
Space | Chooses the focused radio. |
RadioButton accessibility
- A real
<input type="radio">, so the checked state, the roving tab stop across anameand form submission are the platform's rather than this component's. labelrenders the<label>wrapping the input, which is what names this option. What names the *set* is aRadioGroup, or a<fieldset>with a legend of your own.
Swept with axe on every release, in this state:
RadioButton. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.RadioButton style tree
Every part the component draws is a node with a name, so a default can be restyled with
Box.components() instead of a selector — and a variant is a name too.radioButtonRadioGroup props
Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on /box rather than repeated here.
| Prop | Type | Default | What it does |
|---|---|---|---|
label | React.ReactNode | — | The group's name, rendered above it and used as the group's accessible name. |
name | string | — | The name every radio in the group submits under. Generated when left out. |
value | string | — | Controlled selection. Leave it out and the group owns it. |
defaultValue | string | — | The option chosen to begin with, when the group owns its value. |
onValueChange | ChangeHandler<string | undefined, RadioGroupReason> | — | Fires with the new value and why it changed — 'click' or 'keyboard'. A group can also hold nothing, so the value is string | undefined. |
onChange | ChangeHandler<string | undefined, RadioGroupReason> | — | The same callback under its old name. Both fire, so either one can be passed. |
orientation | 'vertical' | 'horizontal' | 'vertical' | Which way the radios stack. Both arrow pairs navigate either way, as APG specifies. |
children | React.ReactNode | — | The options: RadioGroup.Item elements, or anything else the group should hold. |
RadioGroup.Item
A radio that reads its group: the shared
name, whether it is selected, and what to report when it is chosen. Outside a RadioGroup it is a plain RadioButton.| Prop | Type | Default | What it does |
|---|---|---|---|
valuerequired | string | — | What this radio submits, and what the group reports when it is chosen. |
name | string | — | Only for an item rendered outside a group — inside one, the group supplies the name. |
label | React.ReactNode | — | The text beside the radio, rendered inside a label that wraps the input. |
labelProps | BoxProps<'label'> | — | Styles for the wrapping <label>: the row's layout, not the radio's own appearance. |
props | BoxTagProps<'input'> | — | Attributes for the <input> itself. type, name and checked are the group's. |
ref | Ref<HTMLInputElement> | — | The <input> element, for a caller that has to focus or measure this option. |
RadioGroup keyboard
| Key | Result |
|---|---|
Tab | Enters the group once, landing on the chosen option — or on the first, when none is chosen. Tab again leaves the group entirely. |
Down / Right | The next option, choosing it as focus arrives, wrapping to the first. |
Up / Left | The previous option, the same way, wrapping to the last. |
Space | Chooses the focused option. The platform supplies this one. |
RadioGroup accessibility
Implements the WAI-ARIA APG pattern.
role="radiogroup", named by its ownlabelthrougharia-labelledby— radios with nothing over them are unrelated controls to a screen reader.aria-orientationfollowsorientation, and every item shares one generatedname, so the set submits as one field.- The tab order is deliberately the platform's: a native radio set is already one tab stop with a roving tabindex, which a
tabIndexof ours would fight.
Swept with axe on every release, in this state:
RadioGroup. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.RadioGroup style tree
Every part the component draws is a node with a name, so a default can be restyled with
Box.components() instead of a selector — and a variant is a name too.radioGroupvariants: horizontalradioGroup.label