RadioGroup
Context-based radio group; the group owns name, checked state, and change handling.
import { RadioGroup, RadioGroupItem } from '@elirobinson/react/components/atoms/RadioGroup';Styles: @elirobinson/react/styles/molecules/RuleLink.css — already included when you import @elirobinson/react/styles.css.
Show code
import { RadioGroup, RadioGroupItem } from '@elirobinson/react/components/atoms/RadioGroup';
export default function Basic() {
return (
<RadioGroup name="plan" defaultValue="pro">
<RadioGroupItem value="free" label="Free" />
<RadioGroupItem value="pro" label="Pro" />
<RadioGroupItem value="enterprise" label="Enterprise" />
</RadioGroup>
);
}When to use it
Use RadioGroup for a small, always-visible set of mutually exclusive options — two to
six is the sweet spot. Once the list gets long enough to need scrolling or search, reach
for Select or Combobox instead.
RadioGroup is context-based: the group owns name, the current value, and change
handling. RadioGroupItem only takes value and label — it can't take checked,
onChange, or name directly, so there's no way to desync an item from its group.
Rendering a RadioGroupItem outside a RadioGroup throws immediately; the two aren't
meant to be used apart.
Controlled
Pass value and onValueChange to drive selection from your own state. Passing
defaultValue instead makes it uncontrolled — don't pass both.
Booking a 60-minute session.
Show code
import { useState } from 'react';
import { RadioGroup, RadioGroupItem } from '@elirobinson/react/components/atoms/RadioGroup';
export default function Controlled() {
const [duration, setDuration] = useState('60');
return (
<div className="demo-col">
<RadioGroup name="session-length" value={duration} onValueChange={setDuration}>
<RadioGroupItem value="30" label="30 minutes" />
<RadioGroupItem value="60" label="60 minutes" />
<RadioGroupItem value="90" label="90 minutes" />
</RadioGroup>
<p>Booking a {duration}-minute session.</p>
</div>
);
}Clearing a controlled group
value distinguishes empty from absent, and the difference decides the mode:
null— controlled, nothing selected. This is how you clear a controlled group.undefined— uncontrolled. The group falls back todefaultValueand its own internal state, exactly as ifvaluehad never been passed.
So type your state string | null, not string | undefined:
const [plan, setPlan] = useState<string | null>(null);
<RadioGroup name="plan" value={plan} onValueChange={setPlan}>
<RadioGroupItem value="free" label="Free" />
<RadioGroupItem value="pro" label="Pro" />
</RadioGroup>
<button onClick={() => setPlan(null)}>Clear</button>;Clearing with undefined instead does not clear the group — it hands selection back to
the group's own state, which still holds the last click. TypeScript can't catch that,
since undefined is legal for an optional prop, so the group warns in development when
value goes from a string to undefined.
Form submission
The group works with native form submission on its own — no hidden input, no extra
wiring. RadioGroupItem renders a real input[type="radio"], so the group's name is
also the submitted field name, and the selection is in FormData and in a server
action's payload under that name:
<form action={savePlan}>
<RadioGroup name="plan" defaultValue="free">
<RadioGroupItem value="free" label="Free" />
<RadioGroupItem value="pro" label="Pro" />
</RadioGroup>
<Button type="submit">Save</Button>
</form>formData.get('plan') is 'free' or 'pro'. This holds in both modes — a controlled
group submits its current selection too.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
namerequired | string | — | Shared `name` for every item's `input[type="radio"]`, which makes it the field name the group submits under. The group participates in native form submission with no extra wiring: inside a `<form>` its selection shows up in `FormData` and in a server action's payload under this `name`. Nothing hidden needs to be added to carry the value across. |
defaultValue | string | number | readonly string[] | — | — |
onValueChange | ((value: string) => void) | — | — |
value | string | null | — | Selection, when the group is controlled. `null` means controlled with nothing selected; `undefined` means uncontrolled, and hands the selection back to `defaultValue` and the group's own state. The distinction matters: clear a controlled group with `null`, never with `undefined`. |
Also accepts all Omit<HTMLAttributes<HTMLDivElement>, 'role'> props.
RadioGroupItem
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | string | — | — |
defaultValue | string | number | readonly string[] | — | — |
value | string | number | readonly string[] | — | — |
Also accepts all Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'name' | 'checked' | 'onChange'> props.
Extraction notes: ds-radio-group styles are defined in molecules/RuleLink.css, not a RadioGroup sheet.
Accessibility
- The wrapper renders
role="radiogroup"; eachRadioGroupItemrenders a nativeinput[type="radio"]sharing the group'sname. - Keyboard: because the inputs share a native
name, arrow keys move between items and select as they go, andTabtreats the whole group as a single stop — that's the browser's built-in radio-group behavior, not something this component reimplements. RadioGrouphas nolabelprop of its own — introduce it with a standaloneLabelconnected viaaria-labelledby(a plaindivisn't a labelable element, sohtmlFordoesn't reliably associate here).- The ref on
RadioGroupforwards to the wrapping<div>; the ref onRadioGroupItemforwards to its<input>.
Do
- Give RadioGroup a name and either defaultValue (uncontrolled) or value + onValueChange (controlled) — pick one mode.
- Type controlled state as string | null and clear it with null — undefined means uncontrolled.
- Rely on the group name for form submission; it submits natively, so no hidden input is needed.
- Use it for 2–6 mutually exclusive, always-visible options.
- Pair a standalone Label above the group via aria-labelledby, since RadioGroup has no label prop of its own.
- Give every RadioGroupItem a distinct, real value and label.
Don't
- Try to pass checked, onChange, or name to a RadioGroupItem — the type doesn’t allow it, and the group owns all three.
- Render a RadioGroupItem outside a RadioGroup — it throws immediately, with no fallback behavior.
- Reach for RadioGroup when the option list is long or needs search — use Select or Combobox.
- Pass both value and defaultValue — value wins, and defaultValue is only read on the first render.
- Clear a controlled group with undefined — that switches it to uncontrolled and keeps the last selection. Pass null.
- Add a hidden input to carry the selection to a server action — the group already submits under its own name.