Radio Group
Chooses exactly one option from a small, visible set. Built on Base UI
RadioGroup + Radio, with arrow-key navigation between options. When the set
is long or space is tight, prefer Select.
Import
import {
RadioGroup,
Radio,
RadioCard,
} from "@photon-ai/pho-ui/components/radio-group";
Basic
Each Radio needs a value; label makes the whole row selectable. Leave the
group uncontrolled with defaultValue.
<RadioGroup defaultValue="standard">
<Radio value="standard" label="Standard — 3–5 business days" />
<Radio value="express" label="Express — next business day" />
<Radio value="overnight" label="Overnight" />
</RadioGroup>
Disabled
Set disabled on a single Radio to remove just that option, or on
RadioGroup to disable every option at once. Disabled options are skipped by
arrow-key navigation.
<Radio value="overnight" label="Overnight — not available" disabled />
Controlled
Drive the selection yourself with value and onValueChange when the choice
feeds other state.
Selected: standard
const [value, setValue] = useState("standard");
<RadioGroup value={value} onValueChange={setValue}>
<Radio value="standard" label="Standard" />
<Radio value="express" label="Express" />
<Radio value="overnight" label="Overnight" />
</RadioGroup>;
Card
RadioCard wraps the same Radio in a bordered, selectable tile — pass
title and an optional description instead of label. Use it when each
option carries enough weight (a plan, a brand type) to deserve more visual
presence than a plain labeled dot. The whole tile is the hit target and
carries the press feedback; arrange a set of them with RadioGroup as usual.
<RadioGroup defaultValue="org" className="flex flex-col gap-2 sm:flex-row">
<RadioCard
value="org"
title="Organization"
description="A registered company or non-profit."
/>
<RadioCard
value="sole"
title="Sole proprietor"
description="An individual with lighter carrier vetting."
/>
</RadioGroup>
Card with icon
Pass icon when the option has a glyph — it stacks above the title and
replaces the radio dot.
<RadioGroup defaultValue="org" className="flex flex-col gap-2 sm:flex-row">
<RadioCard
value="org"
title="Organization"
description="A registered company or non-profit."
icon={<IconBuilding />}
/>
<RadioCard
value="sole"
title="Sole proprietor"
description="An individual with lighter carrier vetting."
icon={<IconUser />}
/>
</RadioGroup>
Anatomy
- RadioGroup — owns the selection and enforces single-choice; holds the
Radio(orRadioCard) items and is the group’s single tab stop - Radio — one option: a dot plus an optional
label, where the whole row selects the value - RadioCard — one option styled as a bordered tile:
title, optionaldescription, and either a stackediconor a radio dot, where the whole tile selects the value
Props
RadioGroup
- value / defaultValue — controlled / uncontrolled selection
- onValueChange —
(value, eventDetails) => void - name — form field name submitted with the value
- disabled — disable the whole group (default
false) - readOnly — block changing the selection (default
false) - required — require a choice before form submit (default
false)
Radio
- value (required) — this option’s value
- label — clickable text beside the dot (or pass
aria-label) - disabled — disable just this option
RadioCard
- value (required) — this option’s value
- title (required) — tile heading
- description — supporting copy shown below the title
- icon —
ReactNode— glyph stacked above the title; when set, it replaces the radio dot - disabled — disable just this option
Accessibility
RadioGroup renders role="radiogroup" and each Radio renders
role="radio" with aria-checked. Passing label wraps the dot and text in a
<label>, so the whole row is both clickable and the option’s accessible name;
without a label, give the Radio an aria-label. required and readOnly
surface as aria-required and aria-readonly on the group. RadioCard
renders its title and description inside the radio tile, so they
compose into the option’s accessible name.
- Tab — move focus into the group, landing on the selected option (or the first)
- Arrow keys — move to the next/previous enabled option and select it
- Space — select the focused option
The group is a single tab stop: roving tabindex keeps only one radio tabbable,
so Tab moves past the group rather than through each option.
Best practices
- Use a radio group when exactly one choice is required and seeing all options
at once helps the decision; collapse to a
Selectpast ~6 options. - Always preselect a sensible default so users aren’t forced to make a choice they may not understand yet.
- Keep labels parallel and scannable — same grammatical shape, shortest text that disambiguates.
- Every option needs a
labelor anaria-label; a bare dot has no accessible name.