Form
Radio group
Lets people choose exactly one option from a short list that stays visible.
Import
import { RadioGroup, RadioGroupItem } from "@booleanpress/ui/radio-group"Usage
Name the group with aria-label, or with a visible legend and aria-labelledby, and name every radio with a Label whose htmlFor is the radio's id. Clicking the label chooses the radio.
import { Label } from "@booleanpress/ui/label"
import { RadioGroup, RadioGroupItem } from "@booleanpress/ui/radio-group"
export function SendingMode() {
return (
<RadioGroup defaultValue="queue" aria-label="Sending mode">
<div className="flex items-center gap-2">
<RadioGroupItem id="mode-instant" value="instant" />
<Label htmlFor="mode-instant">Send at once</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem id="mode-queue" value="queue" />
<Label htmlFor="mode-queue">Send through the queue</Label>
</div>
</RadioGroup>
)
}It is uncontrolled with defaultValue, or controlled with value and onValueChange; the value is a string. The group lays its radios out in a column with a 12 px gap. For a row, pass className="flex gap-6" (and orientation="horizontal", so the arrow keys match what people see). Once one radio is chosen, the group cannot go back to none by a click; use a Select or a Checkbox when "nothing" is a valid answer. For two options that switch something on or off, use Switch.
Examples
Basic
Three options, one chosen, each named by its label.
Controlled
value and onValueChange keep the choice in your state, here shown beneath the group.
Horizontal
A row of short options; orientation="horizontal" tells the arrow keys and assistive technology it is a row.
Disabled
disabled on the group stops every radio; on one RadioGroupItem it stops that radio and the arrow keys skip it.
Invalid
aria-invalid on the group and its radios draws the error border; aria-describedby reads the message with the group's name.
Accessibility
- Semantics
- The group is
role="radiogroup"; each item is abuttonwithrole="radio"andaria-checked. Inside a form, a hidden native input per radio carries the value, so it is submitted with the form. - Labels
- Name the group (
aria-labeloraria-labelledby) and every radio (aLabelwithhtmlFor). Put an error message inaria-describedbyon the group, and setaria-invalidon the group and on each radio. - Focus
- The group is one tab stop: Tab goes to the chosen radio, or to the first when none is chosen, and the other radios are skipped. The arrow keys move focus and choose together. The focus ring shows on keyboard focus, not on click.
- Known limits
- The radio is 16 px. Keep its label beside it, so the label adds to the target and the pair meets WCAG 2.5.8's 24 px.
aria-invalidis not inherited: put it on the group (so the group reads as invalid) and on each radio (for the red border).
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves focus into the group, to the chosen radio, or to the first radio when none is chosen. A second Tab leaves the group. |
| ArrowDownArrowRight | Moves focus to the next radio and chooses it. From the last radio, it wraps to the first. In right-to-left, ArrowLeft moves forward instead of ArrowRight. |
| ArrowUpArrowLeft | Moves focus to the previous radio and chooses it. From the first radio, it wraps to the last. In right-to-left, ArrowRight moves back instead of ArrowLeft. |
| Space | Chooses the focused radio, if it is not already chosen. |
API
RadioGroup
Renders Radix RadioGroup.Root and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | Render the child element instead, with this part's behaviour and classes merged onto it. | |
defaultValue | string | The value it starts with, when it controls itself. | |
dir | "ltr" | "rtl" | Reading direction for the arrow keys. Defaults to the provider's. | |
disabled | boolean | Stops every radio in the group. | |
form | string | ||
loop | boolean | Whether the arrow keys wrap from the last radio to the first. Defaults to true. | |
name | string | The field name, for the value the form submits. | |
onValueChange | ((value: string) => void) | Called with the new value when a radio is chosen. | |
orientation | "horizontal" | "vertical" | horizontal or vertical, for the arrow keys and aria-orientation. It does not change the layout: set that with className. | |
required | boolean | The form cannot be submitted until a radio is chosen. | |
value | string | null | The chosen value, when you control it. Pair it with onValueChange. |
RadioGroupItem
Renders Radix RadioGroup.Item and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | Render the child element instead, with this part's behaviour and classes merged onto it. | |
checked | boolean | ||
required | boolean | The form cannot be submitted until this radio is chosen. | |
value | string | null | The value this radio chooses. |
Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.
Data attributes: data-slot="radio-group" (RadioGroup), data-slot="radio-group-item" (RadioGroupItem).
Theming
Each radio's edge is --control, at least 3:1 against the surface (WCAG 1.4.11); the chosen dot is --primary; focus is --ring at 50 %; invalid is --destructive.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--control | border |
--destructive | border, focus ring |
--input | border, background |
--primary | text, fill |
--ring | border, focus ring |