Persona

Radio

Inputs · 8 props · Updated Sep 22, 2026

Radio is one option in a set, rendered as a native <input type="radio"> with a label. The set is what holds the answer, and nothing in Radio’s own API makes a set: value is its only required prop, and grouping happens outside it. RadioGroupField is what you render — the group, the question above it and the radios inside it — with Radio used only as its children.

  • Circle the control. Selected is a dot inside it; there is no third state, because a radio can only be cleared by choosing another one.
  • Label this option, in words. The question the options answer belongs to the field above the group, not here.

Radio buttons show every alternative at once and let the reader take one. That is the trade: the set costs a row per option, and in exchange nobody has to open anything to find out what the options are. It holds for a few named alternatives, light mode or dark mode, monthly billing or annual, and stops holding as the list grows, where a Select or a Combobox trades the visibility back for the space.

Never use them for yes or no. Two radios labeled Yes and No spend two rows and two readings on a question that one Checkbox answers on submit, or a Toggle answers immediately.

A radio cannot be cleared by pressing it, and nothing else in its group can clear it either: the only thing that turns a radio off is another radio in the same group turning on. A single radio is therefore a switch that goes one way and stays there, whatever the label says.

A shared name is what makes the radios a group

Section titled “A shared name is what makes the radios a group”

Radios belong to one group when they share a name attribute, which reaches the input through Radio’s passthrough props. It matters because without a shared name each radio is a group of one, and any number of them can be selected at once.

RadioGroup is the component that sets it. It clones each child with the group’s name, marks one checked by comparing value, wraps the set in role="radiogroup", and stacks the options unless horizontal puts them in a row. Keep the stack for anything past two or three short labels: stacked, every circle sits on one left edge and the reader scans a column, where a row moves each circle by the length of the label before it.

The cloning is conditional in a way that catches people: a child is only given checked and onChange when it is a Radio element and the group was passed a value or an onChange. Wrap a Radio in a layout div and the div is what gets cloned: it receives the group’s name as a stray attribute, and the radio inside it never reflects the group’s answer.

RadioGroupField adds the question above the options

Section titled “RadioGroupField adds the question above the options”

RadioGroupField is a RadioGroup inside a FormField. It is the one to render, because a radio group is the case where the missing piece is not the id wiring but the question: each radio’s label names an answer, and only the field renders the text those answers are answers to.

It forwards four props to the field — label, helperText, warningText and errorText — and passes everything else to the group. That is narrower than its type: inputId, labelProps, rightActions and the helper, warning and error prop objects are accepted and then land on the group’s <div> instead of reaching the field, where they do nothing. It has no configField at all. Pass the four, plus name, value, onChange, horizontal and size, and treat the rest as absent.

Hand-composing a FormField around a RadioGroup is the way to get the props above that the field component drops, and the only reason to do it.

Pre-select an option only where it is an answer

Section titled “Pre-select an option only where it is an answer”

A pre-selected radio is indistinguishable from one the reader chose, and it is submitted the same way. Default to one when most readers want it and the rest will notice it is wrong; leave the group empty when the choice is consequential enough that “they did not answer” is worth knowing.

Tab reaches a radio group once and the arrow keys move within it, selecting as they move. That comes from type="radio" and a shared name rather than from PDS, which is the practical consequence of keyboard behavior coming from the element: a group built out of divs and click handlers loses it, and so does a group whose members never got the same name, where Tab then stops on every option in turn.

Each radio’s label names one answer and nothing names the question. A RadioGroupField renders the question and still does not solve this: the label it draws is a visible heading, not the group’s accessible name, because the field never gives the group the label’s id and role="radiogroup" on a <div> cannot be labeled by a for attribute. Set aria-label on the field, which does reach the group, and give it the same words as the visible label.

Without it a screen reader reads four answers and never says what they answer.

“Which theme do you prefer?” belongs to the field; “Light”, “Dark” and “System” belong to the radios. When the question leaks into the options, every option repeats it, and the reader reads the shared part three times to find the part that differs.

The rules every label follows are on Writing.

RadioGroupField first, since it is the component you render. Read the seven field props it accepts and drops against the section above:

NameType
label

ReactNode

Renders <Label> above input

inputId

string

ID of the input element. Providing this will automatically link the label to the input.

helperText

ReactNode

Renders <HelperText> below input

errorText

ReactNode

Renders <ErrorText> below input and helper text.

warningText

ReactNode

Renders <WarningText> below input and helper text.

labelProps

FormLabelProps

helperProps

(HelperTextProps & RefAttributes<HTMLDivElement>)

warningProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

errorProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

configFieldInputWidth

string

Override the width of the input area when configField is true. Accepts any valid CSS width value (e.g. ‘33%’, ‘66.67%’). Defaults to ‘50%‘.

rightActions

ReactNode

Renders actions to the right of the label.

defaultValue

any

name

string

value

any

onChange

((value: any) => void)

horizontal

boolean

Direction of radio group

size

md, sm

Size of radio buttons within the group

Also accepts every prop of ComponentPropsWithoutRef<‘div’>.

Radio’s own props, for the children:

NameType
id

string

If not provided, a unique ID will be generated.

onChange

((e: ChangeEvent<HTMLInputElement, Element>) => void)

label

ReactNode

disabled

boolean

checked

boolean

valuerequired

string

data-test

string

size

md, sm

Also accepts every prop of Omit<ComponentPropsWithoutRef<‘input’>, ‘size’>.