Skip to the content
BooleanPress UI

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 a button with role="radio" and aria-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-label or aria-labelledby) and every radio (a Label with htmlFor). Put an error message in aria-describedby on the group, and set aria-invalid on 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-invalid is not inherited: put it on the group (so the group reads as invalid) and on each radio (for the red border).

Keyboard

Keyboard
KeyBehaviour
TabMoves focus into the group, to the chosen radio, or to the first radio when none is chosen. A second Tab leaves the group.
ArrowDownArrowRightMoves 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.
ArrowUpArrowLeftMoves 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.
SpaceChooses the focused radio, if it is not already chosen.

API

RadioGroup

Renders Radix RadioGroup.Root and passes it every other prop.

RadioGroup props
PropTypeDefaultDescription
asChildbooleanRender the child element instead, with this part's behaviour and classes merged onto it.
defaultValuestringThe value it starts with, when it controls itself.
dir"ltr" | "rtl"Reading direction for the arrow keys. Defaults to the provider's.
disabledbooleanStops every radio in the group.
formstring
loopbooleanWhether the arrow keys wrap from the last radio to the first. Defaults to true.
namestringThe 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.
requiredbooleanThe form cannot be submitted until a radio is chosen.
valuestring | nullThe chosen value, when you control it. Pair it with onValueChange.

RadioGroupItem

Renders Radix RadioGroup.Item and passes it every other prop.

RadioGroupItem props
PropTypeDefaultDescription
asChildbooleanRender the child element instead, with this part's behaviour and classes merged onto it.
checkedboolean
requiredbooleanThe form cannot be submitted until this radio is chosen.
valuestring | nullThe 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.

Theme tokens
TokenUsed for
--controlborder
--destructiveborder, focus ring
--inputborder, background
--primarytext, fill
--ringborder, focus ring