RadioGroup
A radio group lets users select one option from a mutually exclusive list. For multi-select, use CheckboxGroup; for a dropdown, use Select.
import { Radio, RadioGroup } from "@opengovsg/oui"export const Example = () => { return ( <RadioGroup label="Select a city" defaultValue="sf"> <Radio value="sf">San Francisco</Radio> <Radio value="ny">New York</Radio> <Radio value="tokyo">Tokyo</Radio> <Radio value="london">London</Radio> </RadioGroup> )}Usage
Use RadioGroup when users must pick exactly one option from a short, static list rendered inline. Use CheckboxGroup when multiple options can be selected. Use Select or ComboBox for longer lists best shown in a dropdown.
import { Radio, RadioGroup } from "@opengovsg/oui"<RadioGroup label="Select a city" defaultValue="sf">
<Radio value="sf">San Francisco</Radio>
<Radio value="ny">New York</Radio>
</RadioGroup>Alternatively, install the component as local source via the shadcn CLI:
npx shadcn@latest add https://oui.open.gov.sg/r/radio-group.jsonpnpm dlx shadcn@latest add https://oui.open.gov.sg/r/radio-group.jsonnpx shadcn@latest add https://oui.open.gov.sg/r/radio-group.jsonbunx --bun shadcn@latest add https://oui.open.gov.sg/r/radio-group.jsonRadioGroup renders a group of mutually exclusive radio buttons, built on React Aria's RadioGroup. It manages selection state, keyboard navigation, and ARIA semantics automatically. The size prop propagates to all child Radio items via context.
If the radio group does not have a visible label, pass aria-label or aria-labelledby to RadioGroup to identify it to assistive technology.
Anatomy
RadioGroup consists of:
- RadioGroup — the group container; manages selection state, propagates size via context
- Radio — one selectable option; renders the circular indicator, label, and optional description
Examples
Sizes
Use the size prop on RadioGroup to change the size of all radio items within the group. The size is propagated to all children via context.
import { Radio, RadioGroup } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex flex-col gap-8"> <RadioGroup label="Extra small (xs)" size="xs" defaultValue="a"> <Radio value="a">Option A</Radio> <Radio value="b">Option B</Radio> </RadioGroup> <RadioGroup label="Small (sm)" size="sm" defaultValue="a"> <Radio value="a">Option A</Radio> <Radio value="b">Option B</Radio> </RadioGroup> <RadioGroup label="Medium (md)" size="md" defaultValue="a"> <Radio value="a">Option A</Radio> <Radio value="b">Option B</Radio> </RadioGroup> </div> )}With Description
Use the description prop on individual Radio items to add helper text below the label.
import { Radio, RadioGroup } from "@opengovsg/oui"export const Example = () => { return ( <RadioGroup label="Payment method" defaultValue="card"> <Radio value="card" description="Pay securely with credit or debit card"> Credit Card </Radio> <Radio value="paypal" description="Fast checkout with your PayPal account" > PayPal </Radio> <Radio value="bank" description="Processing may take 2-3 business days"> Bank Transfer </Radio> </RadioGroup> )}Disabled
Use the isDisabled prop on RadioGroup to disable the entire group, or on individual Radio items to disable specific options.
import { Radio, RadioGroup } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex flex-col gap-8"> <RadioGroup label="Disabled group" isDisabled defaultValue="a"> <Radio value="a">Option A</Radio> <Radio value="b">Option B</Radio> </RadioGroup> <RadioGroup label="Disabled individual items" defaultValue="a"> <Radio value="a" isDisabled> Disabled selected </Radio> <Radio value="b" isDisabled> Disabled unselected </Radio> <Radio value="c">Enabled</Radio> </RadioGroup> </div> )}Invalid
Use the isInvalid prop along with errorMessage to display a validation error.
import { Radio, RadioGroup } from "@opengovsg/oui"export const Example = () => { return ( <RadioGroup label="Select an option" isRequired isInvalid errorMessage="Please select an option." > <Radio value="a">Option A</Radio> <Radio value="b">Option B</Radio> <Radio value="c">Option C</Radio> </RadioGroup> )}Validation
See the Forms guide for end-to-end patterns including React Hook Form + Zod.
isRequired— marks the group as required; validation fires on form submit (native) or immediately (aria).validate— a custom function(value: string) => ValidationError | true | null | undefined. Return a string to show as the error, ornull/truefor valid.isInvalid+errorMessage— use together for realtime validation feedback without waiting for a form submit.errorMessageaccepts astringor a function(validation: ValidationResult) => string.validationBehavior—"native"(default) blocks form submission when invalid;"aria"only marks the group via ARIA attributes and allows submission — required for React Hook Form.
Events
onChange— Called when the selected value changes, receiving the newstringvalue.onFocus/onBlur— Called when any radio button in the group receives or loses focus.onFocusChange— Called when the focus state of the group changes, receiving a boolean.
Accessibility
- RadioGroup is built on a
<div role="radiogroup">with full ARIA semantics managed by React Aria. - The group label, description, and error message are automatically wired via
aria-labelledbyandaria-describedby. - If no visible label is provided, pass
aria-labeloraria-labelledbytoRadioGroup. - Keyboard interaction: Tab to move focus to the group; Up/Down or Left/Right arrows to move between options; the focused option is automatically selected.
- Individual radio items are not in the tab order once one is selected — only the selected (or first) item receives focus on Tab.
Slots
Slots are named regions of the component you can target with custom Tailwind classes via the classNames prop. Each slot below corresponds to a key on the classNames object.
RadioGroup slots:
base: The root flex-column container wrapping the label, radio items, description, and error.label: The label element rendered above the radio items.description: The helper text rendered below the radio items.error: Nested object of FieldError slots — pass{ icon: "…", text: "…" }to style the error icon and text independently.
Radio slots (via Radio's classNames):
base: The radio row wrapper; handles alignment, spacing, and focus ring.circle: The circular border element that represents the radio button.icon: The inner filled circle shown when the option is selected.label: The label text next to the radio circle.description: The optional descriptive text rendered below the label.
Custom Styles
import { Radio, RadioGroup } from "@opengovsg/oui"export const Example = () => { return ( <RadioGroup label="Select a plan" defaultValue="basic" classNames={{ base: "gap-4", }} > <Radio value="basic" description="For personal use" classNames={{ base: "border rounded-lg border-gray-200 p-4 data-[selected]:border-interaction-main-default", }} > Basic </Radio> <Radio value="pro" description="For teams and businesses" classNames={{ base: "border rounded-lg border-gray-200 p-4 data-[selected]:border-interaction-main-default", }} > Pro </Radio> </RadioGroup> )}Props
RadioGroup
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | The Radio items in the group |
label | string | - | The visible label for the group |
value | string | - | The currently selected value (controlled) |
defaultValue | string | - | The default selected value (uncontrolled) |
onChange | (value: string) => void | - | Called when the selected value changes |
isDisabled | boolean | false | Whether the entire group is disabled |
isReadOnly | boolean | false | Whether the group is read-only |
isRequired | boolean | false | Whether the group is required |
isInvalid | boolean | false | Whether the group should display as invalid |
validate | (value: string) => ValidationError | true | null | undefined | - | Custom validation function |
errorMessage | string | ((validation: ValidationResult) => string) | - | Error message to display when invalid |
validationBehavior | "native" | "aria" | "native" | Whether to use native HTML form validation or ARIA-only |
size | "xs" | "sm" | "md" | "md" | The size of all child Radio items |
description | string | - | Helper text shown below the radio items |
classNames | SlotsToClasses<"base" | "label" | "description"> & { error?: SlotsToClasses<FieldErrorSlots> } | - | Custom Tailwind classes for component slots |
name | string | - | The HTML name attribute; used for form submission |
orientation | "horizontal" | "vertical" | "vertical" | The layout orientation of the radio items |
Inherits all remaining props from React Aria's RadioGroup. Notable ones: autoFocus, excludeFromTabOrder, form.
errorMessage render-prop argument
When errorMessage is a function, it receives a ValidationResult object:
| Property | Type | Description |
|---|---|---|
isInvalid | boolean | Whether the field is currently invalid |
validationErrors | string[] | List of validation error strings |
validationDetails | ValidityState | The native HTML ValidityState object |
Radio
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | ((renderProps: RadioRenderProps) => ReactNode) | - | The label content, or a render function |
value | string | - | The value submitted to the form and passed to onChange |
description | string | - | Optional descriptive text rendered below the label |
isDisabled | boolean | false | Whether this specific radio option is disabled |
classNames | SlotsToClasses<"base" | "circle" | "icon" | "label" | "description"> | - | Custom Tailwind classes for radio slots |
Inherits all remaining props from React Aria's Radio. Notable ones: autoFocus, id.