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 '@cronocode/react-box/components/radioGroup';
import RadioButton from '@cronocode/react-box/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.

Keyboard

KeyResult
TabEnters the group once, landing on the checked option — or the first, when none is checked.
Down / RightNext option, selecting it as focus arrives. Wraps to the first.
Up / LeftPrevious option, same. Wraps to the last.
SpaceSelects the focused option (the platform supplies this one).
Tab, againLeaves the group entirely — a radio set is one stop, not one per option.
A disabled optionSkipped by the arrows, never landed on.
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}
  onChange={(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 />