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.
PropTypeDefaultWhat it does
namestring—The name the radios of one set share — it is what makes them one choice rather than several.
propsRadioButtonTagProps—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.
valuestring | number—What the set submits when this option is the chosen one.
autoFocusboolean—Focus it on mount. One per page at most — a stolen focus is a lost place in the document.
readOnlyboolean—Rendered and read, but not selectable. Unlike disabled it stays in the tab order and submits.
defaultCheckedboolean—Makes this the chosen option to begin with, when the set is uncontrolled.
labelReact.ReactNode—The text beside the radio, rendered inside a <label> that wraps the input.
labelPropsBoxProps<'label'>—Styles for the wrapping <label>: the row's layout, not the radio's own appearance.

RadioButton keyboard

KeyResult
TabEnters 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 / LeftThe 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.
SpaceChooses the focused radio.

RadioButton accessibility

  • A real <input type="radio">, so the checked state, the roving tab stop across a name and form submission are the platform's rather than this component's.
  • label renders the <label> wrapping the input, which is what names this option. What names the *set* is a RadioGroup, 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.
radioButton

RadioGroup 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.
PropTypeDefaultWhat it does
labelReact.ReactNode—The group's name, rendered above it and used as the group's accessible name.
namestring—The name every radio in the group submits under. Generated when left out.
valuestring—Controlled selection. Leave it out and the group owns it.
defaultValuestring—The option chosen to begin with, when the group owns its value.
onValueChangeChangeHandler<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.
onChangeChangeHandler<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.
childrenReact.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.
PropTypeDefaultWhat it does
valuerequiredstring—What this radio submits, and what the group reports when it is chosen.
namestring—Only for an item rendered outside a group — inside one, the group supplies the name.
labelReact.ReactNode—The text beside the radio, rendered inside a label that wraps the input.
labelPropsBoxProps<'label'>—Styles for the wrapping <label>: the row's layout, not the radio's own appearance.
propsBoxTagProps<'input'>—Attributes for the <input> itself. type, name and checked are the group's.
refRef<HTMLInputElement>—The <input> element, for a caller that has to focus or measure this option.

RadioGroup keyboard

KeyResult
TabEnters the group once, landing on the chosen option — or on the first, when none is chosen. Tab again leaves the group entirely.
Down / RightThe next option, choosing it as focus arrives, wrapping to the first.
Up / LeftThe previous option, the same way, wrapping to the last.
SpaceChooses the focused option. The platform supplies this one.

RadioGroup accessibility

  • role="radiogroup", named by its own label through aria-labelledby — radios with nothing over them are unrelated controls to a screen reader.
  • aria-orientation follows orientation, and every item shares one generated name, 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 tabIndex of 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: horizontal
radioGroup.label