Radio Group
A set of mutually exclusive options. Real <input type="radio"> elements sharing a name, so both exclusivity and arrow-key navigation come from the browser.
Plan
Usage#
import { RadioGroup, RadioGroupItem } from "neelam-ui";
<RadioGroup defaultValue="pro" aria-labelledby="plan-label">
<label>
<RadioGroupItem value="starter" />
Starter
</label>
<label>
<RadioGroupItem value="pro" />
Pro
</label>
</RadioGroup>Labelling the group#
RadioGroup renders <div role="radiogroup">, which carries no name of its
own. Point aria-labelledby at the visible heading, or pass aria-label if
there isn't one — what a radio group represents is specific to each use, so it
is not something the component could sensibly hardcode.
An unnamed radiogroup announces nothing
Without aria-label or aria-labelledby, a screen reader reads out each
option but never says what the choice is about. This is the single most
common mistake with this component.
Individual options are labelled the ordinary way — an <label> wrapping the
item and its text. There is no bundled per-item label, for the same reason
Checkbox has none.
The shared name#
Every RadioGroupItem inherits one name from the group, generated with
useId unless you pass your own. That single attribute is what makes the
browser treat them as one group — selecting one clears the rest, and the arrow
keys move between them — with no key handling of our own.
Pass name explicitly when the group is inside a real <form> and you need a
stable field name in the submitted data.
Controlled#
const [plan, setPlan] = useState("pro");
<RadioGroup value={plan} onValueChange={setPlan} aria-label="Plan">…</RadioGroup>disabled on the group disables every option; an individual RadioGroupItem
can still set its own.
Keyboard#
| Key | Behaviour |
|---|---|
| Tab | Moves focus into the group — to the selected option, or the first one if nothing is selected. |
| ↓→ | Selects the next option, wrapping at the end. |
| ↑← | Selects the previous option, wrapping at the start. |
| Space | Selects the focused option, when the group had nothing selected. |
All of it is native radio-input behaviour from the shared name — none of these
keys are intercepted.
Accessibility#
- The group is
role="radiogroup"; each option is a native<input type="radio">, so role, selection, and form participation come from the platform. - The whole group is a single tab stop.
Tabenters and leaves it; the arrow keys move within. This is the roving-focus behaviour the APG specifies, and here it is the browser's own. - The selected dot is
aria-hidden— the input'scheckedproperty already carries the state. - Selection follows focus, as it does for native radios. If choosing an option triggers something expensive, debounce the effect rather than changing the interaction.
API reference#
RadioGroup#
| Prop | Type | Default |
|---|---|---|
defaultValue | string | — |
disabledDisables every item in the group. An individual `RadioGroupItem` can still override this itself. | boolean | — |
nameShared across every `RadioGroupItem` so the browser's own radio-group behavior (mutual exclusivity, arrow-key navigation between them) applies. Omit to have one generated. | string | — |
onValueChange | ((value: string) => void) | — |
value | string | — |
RadioGroupItem#
| Prop | Type | Default |
|---|---|---|
defaultValue | string | — |
disabledDisables every item in the group. An individual `RadioGroupItem` can still override this itself. | boolean | — |
nameShared across every `RadioGroupItem` so the browser's own radio-group behavior (mutual exclusivity, arrow-key navigation between them) applies. Omit to have one generated. | string | — |
onValueChange | ((value: string) => void) | — |
value | string | — |