useRadioGroup

Provides the behavior and accessibility implementation for a radio group component. Radio groups allow users to select a single item from a list of mutually exclusive options.

installyarn add react-aria
version3.23.1
usageimport {useRadioGroup, useRadio} from 'react-aria'

API#


useRadioGroup( (props: AriaRadioGroupProps, , state: RadioGroupState )): RadioGroupAria useRadio( props: AriaRadioProps, state: RadioGroupState, ref: RefObject<HTMLInputElement> ): RadioAria

Features#


Radio groups can be built in HTML with the <fieldset> and <input> elements, however these can be difficult to style. useRadioGroup and useRadio help achieve accessible radio groups that can be styled as needed.

  • Radio groups are exposed to assistive technology via ARIA
  • Each radio is built with a native HTML <input> element, which can be optionally visually hidden to allow custom styling
  • Full support for browser features like form autofill
  • Keyboard focus management and cross browser normalization
  • Group and radio labeling support for assistive technology

Anatomy#


CatDogInputFavorite PetRadio group labelDragonRadio groupRadio label

A radio group consists of a set of radio buttons, and a label. Each radio includes a label and a visual selection indicator. A single radio button within the group can be selected at a time. Users may click or touch a radio button to select it, or use the Tab key to navigate to the group, the arrow keys to navigate within the group, and the Space key to select an option.

useRadioGroup returns props for the group and its label, which you should spread onto the appropriate element:

NameTypeDescription
radioGroupPropsDOMAttributesProps for the radio group wrapper element.
labelPropsDOMAttributesProps for the radio group's visible label (if any).
descriptionPropsDOMAttributesProps for the radio group description element, if any.
errorMessagePropsDOMAttributesProps for the radio group error message element, if any.

useRadio returns props for an individual radio, along with states that can be used for styling:

NameTypeDescription
inputPropsInputHTMLAttributes<HTMLInputElement>Props for the input element.
isDisabledbooleanWhether the radio is disabled.
isSelectedbooleanWhether the radio is currently selected.
isPressedbooleanWhether the radio is in a pressed state.

Selection state is managed by the useRadioGroupState hook in @react-stately/radio. The state object should be passed as an option to useRadio.

Individual radio buttons must have a visual label. If the radio group does not have a visible label, an aria-label or aria-labelledby prop must be passed instead to identify the element to assistive technology.

Example#


This example uses native input elements for the radios, and React context to share state from the group to each radio. An HTML <label> element wraps the native input and the text to provide an implicit label for the radio.

import {useRadioGroupState} from 'react-stately';
import {useRadio, useRadioGroup} from 'react-aria';

let RadioContext = React.createContext(null);

function RadioGroup(props) {
  let { children, label, description, errorMessage, validationState } = props;
  let state = useRadioGroupState(props);
  let { radioGroupProps, labelProps, descriptionProps, errorMessageProps } =
    useRadioGroup(props, state);

  return (
    <div {...radioGroupProps}>
      <span {...labelProps}>{label}</span>
      <RadioContext.Provider value={state}>
        {children}
      </RadioContext.Provider>
      {description && (
        <div {...descriptionProps} style={{ fontSize: 12 }}>{description}</div>
      )}
      {errorMessage && validationState === 'invalid' &&
        (
          <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }}>
            {errorMessage}
          </div>
        )}
    </div>
  );
}

function Radio(props) {
  let { children } = props;
  let state = React.useContext(RadioContext);
  let ref = React.useRef(null);
  let { inputProps } = useRadio(props, state, ref);

  return (
    <label style={{ display: 'block' }}>
      <input {...inputProps} ref={ref} />
      {children}
    </label>
  );
}

<RadioGroup label="Favorite pet">
  <Radio value="dogs">Dogs</Radio>
  <Radio value="cats">Cats</Radio>
</RadioGroup>
import {useRadioGroupState} from 'react-stately';
import {useRadio, useRadioGroup} from 'react-aria';

let RadioContext = React.createContext(null);

function RadioGroup(props) {
  let {
    children,
    label,
    description,
    errorMessage,
    validationState
  } = props;
  let state = useRadioGroupState(props);
  let {
    radioGroupProps,
    labelProps,
    descriptionProps,
    errorMessageProps
  } = useRadioGroup(props, state);

  return (
    <div {...radioGroupProps}>
      <span {...labelProps}>{label}</span>
      <RadioContext.Provider value={state}>
        {children}
      </RadioContext.Provider>
      {description && (
        <div {...descriptionProps} style={{ fontSize: 12 }}>
          {description}
        </div>
      )}
      {errorMessage && validationState === 'invalid' &&
        (
          <div
            {...errorMessageProps}
            style={{ color: 'red', fontSize: 12 }}
          >
            {errorMessage}
          </div>
        )}
    </div>
  );
}

function Radio(props) {
  let { children } = props;
  let state = React.useContext(RadioContext);
  let ref = React.useRef(null);
  let { inputProps } = useRadio(props, state, ref);

  return (
    <label style={{ display: 'block' }}>
      <input {...inputProps} ref={ref} />
      {children}
    </label>
  );
}

<RadioGroup label="Favorite pet">
  <Radio value="dogs">Dogs</Radio>
  <Radio value="cats">Cats</Radio>
</RadioGroup>
import {useRadioGroupState} from 'react-stately';
import {
  useRadio,
  useRadioGroup
} from 'react-aria';

let RadioContext = React
  .createContext(null);

function RadioGroup(
  props
) {
  let {
    children,
    label,
    description,
    errorMessage,
    validationState
  } = props;
  let state =
    useRadioGroupState(
      props
    );
  let {
    radioGroupProps,
    labelProps,
    descriptionProps,
    errorMessageProps
  } = useRadioGroup(
    props,
    state
  );

  return (
    <div
      {...radioGroupProps}
    >
      <span
        {...labelProps}
      >
        {label}
      </span>
      <RadioContext.Provider
        value={state}
      >
        {children}
      </RadioContext.Provider>
      {description && (
        <div
          {...descriptionProps}
          style={{
            fontSize: 12
          }}
        >
          {description}
        </div>
      )}
      {errorMessage &&
        validationState ===
          'invalid' &&
        (
          <div
            {...errorMessageProps}
            style={{
              color:
                'red',
              fontSize:
                12
            }}
          >
            {errorMessage}
          </div>
        )}
    </div>
  );
}

function Radio(props) {
  let { children } =
    props;
  let state = React
    .useContext(
      RadioContext
    );
  let ref = React.useRef(
    null
  );
  let { inputProps } =
    useRadio(
      props,
      state,
      ref
    );

  return (
    <label
      style={{
        display: 'block'
      }}
    >
      <input
        {...inputProps}
        ref={ref}
      />
      {children}
    </label>
  );
}

<RadioGroup label="Favorite pet">
  <Radio value="dogs">
    Dogs
  </Radio>
  <Radio value="cats">
    Cats
  </Radio>
</RadioGroup>

Styling#


To build a custom styled radio button, you can make the native input element visually hidden. This is possible using the <VisuallyHidden> utility component from @react-aria/visually-hidden. It is still in the DOM and accessible to assistive technology, but invisible. This example uses SVG to build the visual radio button, which is hidden from screen readers with aria-hidden.

For keyboard accessibility, a focus ring is important to indicate which element has keyboard focus. This is implemented with the useFocusRing hook from @react-aria/focus. When isFocusVisible is true, an extra SVG element is rendered to indicate focus. The focus ring is only visible when the user is interacting with a keyboard, not with a mouse or touch.

import {useFocusRing, VisuallyHidden} from 'react-aria';

// RadioGroup is the same as in the previous example
let RadioContext = React.createContext(null);

function RadioGroup(props) {
  let { children, label, description, errorMessage, validationState } = props;
  let state = useRadioGroupState(props);
  let { radioGroupProps, labelProps, descriptionProps, errorMessageProps } =
    useRadioGroup(props, state);

  return (
    <div {...radioGroupProps}>
      <span {...labelProps}>{label}</span>
      <RadioContext.Provider value={state}>
        {children}
      </RadioContext.Provider>
      {description && (
        <div {...descriptionProps} style={{ fontSize: 12 }}>{description}</div>
      )}
      {errorMessage && validationState === 'invalid' &&
        (
          <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }}>
            {errorMessage}
          </div>
        )}
    </div>
  );
}

function Radio(props) {
  let { children } = props;
  let state = React.useContext(RadioContext);
  let ref = React.useRef(null);
  let { inputProps, isSelected, isDisabled } = useRadio(props, state, ref);
  let { isFocusVisible, focusProps } = useFocusRing();
  let strokeWidth = isSelected ? 6 : 2;

  return (
    <label
      style={{
        display: 'flex',
        alignItems: 'center',
        opacity: isDisabled ? 0.4 : 1
      }}
    >
      <VisuallyHidden>
        <input {...inputProps} {...focusProps} ref={ref} />
      </VisuallyHidden>
      <svg
        width={24}
        height={24}
        aria-hidden="true"
        style={{ marginRight: 4 }}
      >
        <circle
          cx={12}
          cy={12}
          r={8 - strokeWidth / 2}
          fill="none"
          stroke={isSelected ? 'orange' : 'gray'}
          strokeWidth={strokeWidth}
        />
        {isFocusVisible &&
          (
            <circle
              cx={12}
              cy={12}
              r={11}
              fill="none"
              stroke="orange"
              strokeWidth={2}
            />
          )}
      </svg>
      {children}
    </label>
  );
}

<RadioGroup label="Favorite pet">
  <Radio value="dogs">Dogs</Radio>
  <Radio value="cats">Cats</Radio>
</RadioGroup>
import {useFocusRing, VisuallyHidden} from 'react-aria';

// RadioGroup is the same as in the previous example
let RadioContext = React.createContext(null);

function RadioGroup(props) {
  let {
    children,
    label,
    description,
    errorMessage,
    validationState
  } = props;
  let state = useRadioGroupState(props);
  let {
    radioGroupProps,
    labelProps,
    descriptionProps,
    errorMessageProps
  } = useRadioGroup(props, state);

  return (
    <div {...radioGroupProps}>
      <span {...labelProps}>{label}</span>
      <RadioContext.Provider value={state}>
        {children}
      </RadioContext.Provider>
      {description && (
        <div {...descriptionProps} style={{ fontSize: 12 }}>
          {description}
        </div>
      )}
      {errorMessage && validationState === 'invalid' &&
        (
          <div
            {...errorMessageProps}
            style={{ color: 'red', fontSize: 12 }}
          >
            {errorMessage}
          </div>
        )}
    </div>
  );
}

function Radio(props) {
  let { children } = props;
  let state = React.useContext(RadioContext);
  let ref = React.useRef(null);
  let { inputProps, isSelected, isDisabled } = useRadio(
    props,
    state,
    ref
  );
  let { isFocusVisible, focusProps } = useFocusRing();
  let strokeWidth = isSelected ? 6 : 2;

  return (
    <label
      style={{
        display: 'flex',
        alignItems: 'center',
        opacity: isDisabled ? 0.4 : 1
      }}
    >
      <VisuallyHidden>
        <input {...inputProps} {...focusProps} ref={ref} />
      </VisuallyHidden>
      <svg
        width={24}
        height={24}
        aria-hidden="true"
        style={{ marginRight: 4 }}
      >
        <circle
          cx={12}
          cy={12}
          r={8 - strokeWidth / 2}
          fill="none"
          stroke={isSelected ? 'orange' : 'gray'}
          strokeWidth={strokeWidth}
        />
        {isFocusVisible &&
          (
            <circle
              cx={12}
              cy={12}
              r={11}
              fill="none"
              stroke="orange"
              strokeWidth={2}
            />
          )}
      </svg>
      {children}
    </label>
  );
}

<RadioGroup label="Favorite pet">
  <Radio value="dogs">Dogs</Radio>
  <Radio value="cats">Cats</Radio>
</RadioGroup>
import {
  useFocusRing,
  VisuallyHidden
} from 'react-aria';

// RadioGroup is the same as in the previous example
let RadioContext = React
  .createContext(null);

function RadioGroup(
  props
) {
  let {
    children,
    label,
    description,
    errorMessage,
    validationState
  } = props;
  let state =
    useRadioGroupState(
      props
    );
  let {
    radioGroupProps,
    labelProps,
    descriptionProps,
    errorMessageProps
  } = useRadioGroup(
    props,
    state
  );

  return (
    <div
      {...radioGroupProps}
    >
      <span
        {...labelProps}
      >
        {label}
      </span>
      <RadioContext.Provider
        value={state}
      >
        {children}
      </RadioContext.Provider>
      {description && (
        <div
          {...descriptionProps}
          style={{
            fontSize: 12
          }}
        >
          {description}
        </div>
      )}
      {errorMessage &&
        validationState ===
          'invalid' &&
        (
          <div
            {...errorMessageProps}
            style={{
              color:
                'red',
              fontSize:
                12
            }}
          >
            {errorMessage}
          </div>
        )}
    </div>
  );
}

function Radio(props) {
  let { children } =
    props;
  let state = React
    .useContext(
      RadioContext
    );
  let ref = React.useRef(
    null
  );
  let {
    inputProps,
    isSelected,
    isDisabled
  } = useRadio(
    props,
    state,
    ref
  );
  let {
    isFocusVisible,
    focusProps
  } = useFocusRing();
  let strokeWidth =
    isSelected ? 6 : 2;

  return (
    <label
      style={{
        display: 'flex',
        alignItems:
          'center',
        opacity:
          isDisabled
            ? 0.4
            : 1
      }}
    >
      <VisuallyHidden>
        <input
          {...inputProps}
          {...focusProps}
          ref={ref}
        />
      </VisuallyHidden>
      <svg
        width={24}
        height={24}
        aria-hidden="true"
        style={{
          marginRight: 4
        }}
      >
        <circle
          cx={12}
          cy={12}
          r={8 -
            strokeWidth /
              2}
          fill="none"
          stroke={isSelected
            ? 'orange'
            : 'gray'}
          strokeWidth={strokeWidth}
        />
        {isFocusVisible &&
          (
            <circle
              cx={12}
              cy={12}
              r={11}
              fill="none"
              stroke="orange"
              strokeWidth={2}
            />
          )}
      </svg>
      {children}
    </label>
  );
}

<RadioGroup label="Favorite pet">
  <Radio value="dogs">
    Dogs
  </Radio>
  <Radio value="cats">
    Cats
  </Radio>
</RadioGroup>

Styled examples#


Swatch Group
A color swatch picker built with Tailwind CSS.
Selectable Cards
A selectable card group built with Styled Components.
Button Group
A single-selectable segmented button group.

Usage#


The following examples show how to use the RadioGroup component created in the above example.

Default value#

An initial, uncontrolled value can be provided to the RadioGroup using the defaultValue prop, which accepts a value corresponding with the value prop of each Radio.

<RadioGroup label="Are you a wizard?" defaultValue="yes">
  <Radio value="yes">Yes</Radio>
  <Radio value="no">No</Radio>
</RadioGroup>
<RadioGroup label="Are you a wizard?" defaultValue="yes">
  <Radio value="yes">Yes</Radio>
  <Radio value="no">No</Radio>
</RadioGroup>
<RadioGroup
  label="Are you a wizard?"
  defaultValue="yes"
>
  <Radio value="yes">
    Yes
  </Radio>
  <Radio value="no">
    No
  </Radio>
</RadioGroup>

Controlled value#

A controlled value can be provided using the value prop, which accepts a value corresponding with the value prop of each Radio. The onChange event is fired when the user selects a radio.

function Example() {
  let [selected, setSelected] = React.useState('');

  return (
    <>
      <RadioGroup
        label="Favorite avatar"
        value={selected}
        onChange={setSelected}
      >
        <Radio value="wizard">Wizard</Radio>
        <Radio value="dragon">Dragon</Radio>
      </RadioGroup>
      <p>You have selected: {selected}</p>
    </>
  );
}
function Example() {
  let [selected, setSelected] = React.useState('');

  return (
    <>
      <RadioGroup
        label="Favorite avatar"
        value={selected}
        onChange={setSelected}
      >
        <Radio value="wizard">Wizard</Radio>
        <Radio value="dragon">Dragon</Radio>
      </RadioGroup>
      <p>You have selected: {selected}</p>
    </>
  );
}
function Example() {
  let [
    selected,
    setSelected
  ] = React.useState('');

  return (
    <>
      <RadioGroup
        label="Favorite avatar"
        value={selected}
        onChange={setSelected}
      >
        <Radio value="wizard">
          Wizard
        </Radio>
        <Radio value="dragon">
          Dragon
        </Radio>
      </RadioGroup>
      <p>
        You have
        selected:{' '}
        {selected}
      </p>
    </>
  );
}

Description#

The description prop can be used to associate additional help text with a radio group.

<RadioGroup label="Favorite pet" description="Select your favorite pet.">
  <Radio value="dogs">Dogs</Radio>
  <Radio value="cats">Cats</Radio>
</RadioGroup>
<RadioGroup
  label="Favorite pet"
  description="Select your favorite pet."
>
  <Radio value="dogs">Dogs</Radio>
  <Radio value="cats">Cats</Radio>
</RadioGroup>
<RadioGroup
  label="Favorite pet"
  description="Select your favorite pet."
>
  <Radio value="dogs">
    Dogs
  </Radio>
  <Radio value="cats">
    Cats
  </Radio>
</RadioGroup>

Error message#

The errorMessage prop can be used to help the user fix a validation error. It should be combined with the validationState prop to semantically mark the radio group as invalid for assistive technologies.

<RadioGroup
  label="Favorite pet"
  errorMessage="Invalid pet selection."
  validationState="invalid"
>
  <Radio value="dogs">Dogs</Radio>
  <Radio value="cats">Cats</Radio>
</RadioGroup>
<RadioGroup
  label="Favorite pet"
  errorMessage="Invalid pet selection."
  validationState="invalid"
>
  <Radio value="dogs">Dogs</Radio>
  <Radio value="cats">Cats</Radio>
</RadioGroup>
<RadioGroup
  label="Favorite pet"
  errorMessage="Invalid pet selection."
  validationState="invalid"
>
  <Radio value="dogs">
    Dogs
  </Radio>
  <Radio value="cats">
    Cats
  </Radio>
</RadioGroup>

Disabled#

The entire RadioGroup can be disabled with the isDisabled prop.

<RadioGroup label="Favorite sport" isDisabled>
  <Radio value="soccer">Soccer</Radio>
  <Radio value="baseball">Baseball</Radio>
  <Radio value="basketball">Basketball</Radio>
</RadioGroup>
<RadioGroup label="Favorite sport" isDisabled>
  <Radio value="soccer">Soccer</Radio>
  <Radio value="baseball">Baseball</Radio>
  <Radio value="basketball">Basketball</Radio>
</RadioGroup>
<RadioGroup
  label="Favorite sport"
  isDisabled
>
  <Radio value="soccer">
    Soccer
  </Radio>
  <Radio value="baseball">
    Baseball
  </Radio>
  <Radio value="basketball">
    Basketball
  </Radio>
</RadioGroup>

To disable an individual radio, pass isDisabled to the Radio instead.

<RadioGroup label="Favorite sport">
  <Radio value="soccer">Soccer</Radio>
  <Radio value="baseball" isDisabled>Baseball</Radio>
  <Radio value="basketball">Basketball</Radio>
</RadioGroup>
<RadioGroup label="Favorite sport">
  <Radio value="soccer">Soccer</Radio>
  <Radio value="baseball" isDisabled>Baseball</Radio>
  <Radio value="basketball">Basketball</Radio>
</RadioGroup>
<RadioGroup label="Favorite sport">
  <Radio value="soccer">
    Soccer
  </Radio>
  <Radio
    value="baseball"
    isDisabled
  >
    Baseball
  </Radio>
  <Radio value="basketball">
    Basketball
  </Radio>
</RadioGroup>

Read only#

The isReadOnly prop makes the selection immutable. Unlike isDisabled, the RadioGroup remains focusable. See the MDN docs for more information.

<RadioGroup label="Favorite avatar" defaultValue="wizard" isReadOnly>
  <Radio value="wizard">Wizard</Radio>
  <Radio value="dragon">Dragon</Radio>
</RadioGroup>
<RadioGroup
  label="Favorite avatar"
  defaultValue="wizard"
  isReadOnly
>
  <Radio value="wizard">Wizard</Radio>
  <Radio value="dragon">Dragon</Radio>
</RadioGroup>
<RadioGroup
  label="Favorite avatar"
  defaultValue="wizard"
  isReadOnly
>
  <Radio value="wizard">
    Wizard
  </Radio>
  <Radio value="dragon">
    Dragon
  </Radio>
</RadioGroup>

HTML forms#

RadioGroup supports the name prop, paired with the Radio value prop, for integration with HTML forms.

<RadioGroup label="Favorite pet" name="pet">
  <Radio value="dogs">Dogs</Radio>
  <Radio value="cats">Cats</Radio>
</RadioGroup>
<RadioGroup label="Favorite pet" name="pet">
  <Radio value="dogs">Dogs</Radio>
  <Radio value="cats">Cats</Radio>
</RadioGroup>
<RadioGroup
  label="Favorite pet"
  name="pet"
>
  <Radio value="dogs">
    Dogs
  </Radio>
  <Radio value="cats">
    Cats
  </Radio>
</RadioGroup>

Internationalization#


RTL#

In right-to-left languages, the radio group and radio buttons should be mirrored. The group should be right-aligned, and the radio should be placed on the right side of the label. Ensure that your CSS accounts for this.