Open UI

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.json
pnpm dlx shadcn@latest add https://oui.open.gov.sg/r/radio-group.json
npx shadcn@latest add https://oui.open.gov.sg/r/radio-group.json
bunx --bun shadcn@latest add https://oui.open.gov.sg/r/radio-group.json

RadioGroup 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, or null/true for valid.
  • isInvalid + errorMessage — use together for realtime validation feedback without waiting for a form submit. errorMessage accepts a string or 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 new string value.
  • 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-labelledby and aria-describedby.
  • If no visible label is provided, pass aria-label or aria-labelledby to RadioGroup.
  • 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

PropTypeDefaultDescription
childrenReactNode-The Radio items in the group
labelstring-The visible label for the group
valuestring-The currently selected value (controlled)
defaultValuestring-The default selected value (uncontrolled)
onChange(value: string) => void-Called when the selected value changes
isDisabledbooleanfalseWhether the entire group is disabled
isReadOnlybooleanfalseWhether the group is read-only
isRequiredbooleanfalseWhether the group is required
isInvalidbooleanfalseWhether the group should display as invalid
validate(value: string) => ValidationError | true | null | undefined-Custom validation function
errorMessagestring | ((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
descriptionstring-Helper text shown below the radio items
classNamesSlotsToClasses<"base" | "label" | "description"> & { error?: SlotsToClasses<FieldErrorSlots> }-Custom Tailwind classes for component slots
namestring-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:

PropertyTypeDescription
isInvalidbooleanWhether the field is currently invalid
validationErrorsstring[]List of validation error strings
validationDetailsValidityStateThe native HTML ValidityState object

Radio

PropTypeDefaultDescription
childrenReactNode | ((renderProps: RadioRenderProps) => ReactNode)-The label content, or a render function
valuestring-The value submitted to the form and passed to onChange
descriptionstring-Optional descriptive text rendered below the label
isDisabledbooleanfalseWhether this specific radio option is disabled
classNamesSlotsToClasses<"base" | "circle" | "icon" | "label" | "description">-Custom Tailwind classes for radio slots

Inherits all remaining props from React Aria's Radio. Notable ones: autoFocus, id.

On this page