A choice of one from a short list, with each option a full-pill row and the chosen one filled ink.
Basic
Pass a label, and a hint if the label cannot say it all. Each Radio can have its own hint under its row. Choose another option to see the Changed chip.
Example
Example
'use client';
import { Radio, RadioGroup } from '@snapshot-ui/react';
import { useState, type ReactElement } from 'react';
const SAVED = 'weekly';
export default function RadioGroupBasic(): ReactElement {
const [value, setValue] = useState<string | null>(SAVED);
return (
<RadioGroup
label="Newsletter frequency"
description="You can change this at any time. Choose another option to see the chip."
name="frequency"
value={value}
onChange={setValue}
isChanged={value !== SAVED}
style={{ width: '100%', maxWidth: '24rem' }}
>
<Radio value="weekly" description="Every Tuesday morning.">
Weekly
</Radio>
<Radio value="monthly" description="The first Tuesday of the month.">
Monthly
</Radio>
<Radio value="never">Never</Radio>
</RadioGroup>
);
}Horizontal
Set orientation="horizontal" for a short row of options. Options keep their width and wrap onto the next line when the row runs out of room.
Example
Example
'use client';
import { Radio, RadioGroup } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function RadioGroupHorizontal(): ReactElement {
return (
<RadioGroup label="Text size" name="text-size" orientation="horizontal" defaultValue="medium">
<Radio value="small">Small</Radio>
<Radio value="medium">Medium</Radio>
<Radio value="large">Large</Radio>
</RadioGroup>
);
}States
- Changed: a clay chip beside the label is the only cue, since a choice has no control edge to recolor. A group with no visible
labelshows no chip. - Invalid: every row gets the error edge, and an error line shows. Invalid outranks changed, so the chip hides.
- Disabled: the group is recessed and keeps the check on its choice. One option can be disabled on its own, with its label in taupe.
Example
Example
'use client';
import { Radio, RadioGroup } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function RadioGroupStates(): ReactElement {
return (
<div className="stack" style={{ width: '100%', maxWidth: '24rem' }}>
<RadioGroup label="Changed" defaultValue="monthly" isChanged>
<Radio value="weekly">Weekly</Radio>
<Radio value="monthly">Monthly</Radio>
</RadioGroup>
<RadioGroup label="Invalid" isInvalid errorMessage="Choose a frequency.">
<Radio value="weekly">Weekly</Radio>
<Radio value="monthly">Monthly</Radio>
</RadioGroup>
<RadioGroup label="Disabled group" defaultValue="weekly" isDisabled>
<Radio value="weekly">Weekly</Radio>
<Radio value="monthly">Monthly</Radio>
</RadioGroup>
<RadioGroup label="Disabled option" defaultValue="free">
<Radio value="free">Free</Radio>
<Radio value="team" isDisabled>
Team
</Radio>
</RadioGroup>
</div>
);
}Validation
Set isRequired or a validate function, and the browser's checks run on submit. The error line shows the message, and choosing an option clears it. The first failed submit moves focus to the group.
Example
Example
'use client';
import { Button, Radio, RadioGroup, Text } from '@snapshot-ui/react';
import { useState, type ReactElement } from 'react';
export default function RadioGroupValidation(): ReactElement {
const [chosen, setChosen] = useState('');
return (
<form
className="stack"
style={{ width: '100%', maxWidth: '24rem' }}
onSubmit={(event) => {
event.preventDefault();
const plan = new FormData(event.currentTarget).get('plan');
setChosen(typeof plan === 'string' ? plan : '');
}}
>
<RadioGroup label="Plan" name="plan" isRequired errorMessage="Choose a plan to continue.">
<Radio value="free">Free</Radio>
<Radio value="team">Team</Radio>
<Radio value="business">Business</Radio>
</RadioGroup>
<div className="row">
<Button type="submit">Continue</Button>
</div>
<Text tone="muted" role="status">
{chosen ? `You chose the ${chosen} plan.` : ''}
</Text>
</form>
);
}Usage
Every value works controlled and uncontrolled. A group with a name submits the chosen value in a plain form. onChange receives the value as a string.
import { useState } from 'react';
import { Radio, RadioGroup } from '@snapshot-ui/react';
export function Frequency({ saved }: { saved: string }) {
const [value, setValue] = useState<string | null>(saved);
return (
<RadioGroup
label="Newsletter frequency"
name="frequency"
value={value}
onChange={setValue}
isChanged={value !== saved}
>
<Radio value="weekly" description="Every Tuesday morning.">
Weekly
</Radio>
<Radio value="monthly">Monthly</Radio>
<Radio value="never">Never</Radio>
</RadioGroup>
);
}The types require a name. A group takes a label, an aria-label or an aria-labelledby. A Radio takes its text, an aria-label or an aria-labelledby.
For an on or off setting, use a switch. For a choice that changes the view in place, such as a mode, use a segmented control.
Props
Every React Aria RadioGroup prop is accepted. These are the ones Snapshot UI adds or shapes.
changedLabel string | The text of the Changed chip. Default: 'Changed' |
|---|---|
defaultValue string | null | The default value (uncontrolled). |
description ReactNode | A hint under the options. It becomes the group's accessible description. |
errorMessage ((validation: ValidationResult) => ReactNode) | ReactNode | The error line, shown only while the group is invalid. A function receives the validation result. Without it the group shows its own validation messages. |
isChanged boolean | Whether the choice differs from the saved one. Shows the clay Changed chip
in the label row, which a screen reader reads with the group. An invalid
group shows its error instead. Without a visible |
isDisabled boolean | Whether the input is disabled. |
isInvalid boolean | Whether the input value is invalid. |
isReadOnly boolean | Whether the input can be selected but not changed by the user. |
isRequired boolean | Whether user input is required on the input before form submission. |
label ReactNode | The group's name, shown above the options. Required unless the group has an |
name string | The name of the input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname). |
onChange ((value: string) => void) | Handler that is called when the value changes. |
orientation Orientation | The axis the Radio Button(s) should align with. Default: 'vertical' |
validate ((value: string) => ValidationError | true | null | undefined) | A function that returns an error message if a given value is invalid.
Validation errors are displayed to the user when the form is submitted
if |
value string | null | The current value (controlled). |
Each option is a Radio.
children ReactNode | The option's label. Long text, even an unbroken word, wraps inside the
row. Required unless the radio has an |
|---|---|
description ReactNode | A hint under the option's row. It becomes the radio's accessible description. |
inputRef Ref<HTMLInputElement> | A ref to the native radio input. |
isDisabled boolean | Whether the radio button is disabled or not. Shows that a selection exists, but is not available in that circumstance. |
value string | The value of the radio button, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio#Value). |
Accessibility
A group of native radio inputs, named by the label. The hint, the error line and, while it shows, the Changed chip describe it. The group is one tab stop, and the arrow keys move the choice. The dot and the check are decoration, so an option's name is its label text.
| Tab | Moves focus into the group, to the chosen option or the first one. The 2px ring sits outside an unchosen row and inside the ink of the chosen one. |
|---|---|
| DownRight | Moves to the next option and chooses it. |
| UpLeft | Moves to the previous option and chooses it. |
| Space | Chooses the focused option. |
- The arrow keys skip disabled options.
- A read-only group keeps its choice and has no hover wash.
- Do not disable the chosen option of a group the reader can still change. Only the chosen radio gets the tab stop, and a disabled one loses it, so Tab skips the group.
A group or option with no name does not compile. TypeScript's message names only aria-labelledby or says the props are not assignable. Either one means a missing accessible name.
Design rules
What keeps radio groups in the system.
Name the group in the label, in sentence case, and keep options short enough to scan.
Choose a default when one is safe. A required group with nothing chosen shows the error only after a failed submit.
Show the chip while the choice is unsaved, and only then.
Never use a radio group for more than about six options. Use a select.
Never leave the chip beside an error. Invalid outranks changed.
Never hide the check. Color is not the only sign of the chosen option.