React hooks for building accessible radio buttons and radio groups with WAI-ARIA compliance.
npx @tessl/cli install tessl/npm-react-aria--radio@3.12.0@react-aria/radio provides React hooks for building accessible radio buttons and radio groups with full WAI-ARIA compliance. It implements the Radio Group pattern with complete keyboard navigation, screen reader support, and mouse/touch interactions while remaining completely unstyled to allow custom styling.
npm install @react-aria/radioimport { useRadio, useRadioGroup } from "@react-aria/radio";
import type { AriaRadioProps, AriaRadioGroupProps, RadioAria, RadioGroupAria, Orientation } from "@react-aria/radio";For CommonJS:
const { useRadio, useRadioGroup } = require("@react-aria/radio");import { useRadio, useRadioGroup } from "@react-aria/radio";
import { useRadioGroupState } from "@react-stately/radio"; // Required peer dependency
import { useRef } from "react";
// Radio Group Component
function MyRadioGroup(props) {
const state = useRadioGroupState(props);
const { radioGroupProps, labelProps } = useRadioGroup(props, state);
return (
<div {...radioGroupProps}>
<span {...labelProps}>{props.label}</span>
{props.children}
</div>
);
}
// Individual Radio Component
function MyRadio(props) {
const ref = useRef();
const state = useRadioGroupState(props.groupProps);
const { inputProps, labelProps, isSelected, isDisabled } = useRadio(props, state, ref);
return (
<label {...labelProps}>
<input {...inputProps} ref={ref} />
{props.children}
</label>
);
}
// Usage
<MyRadioGroup label="Favorite pet" value={selectedPet} onChange={setSelectedPet}>
<MyRadio value="dogs">Dogs</MyRadio>
<MyRadio value="cats">Cats</MyRadio>
<MyRadio value="birds">Birds</MyRadio>
</MyRadioGroup>@react-aria/radio is built around the separation of behavior and presentation:
Provides behavior and accessibility implementation for radio group components that allow users to select a single item from mutually exclusive options.
/**
* Provides the behavior and accessibility implementation for a radio group component.
* @param props - Props for the radio group
* @param state - State for the radio group, as returned by useRadioGroupState
* @returns RadioGroupAria object with props and validation state
*/
function useRadioGroup(props: AriaRadioGroupProps, state: RadioGroupState): RadioGroupAria;
interface AriaRadioGroupProps extends RadioGroupProps, InputDOMProps, DOMProps, AriaLabelingProps, AriaValidationProps {
/** The axis the radio buttons should align with */
orientation?: Orientation;
/** The name of the radio group for form submission */
name?: string;
/** Whether the radio group is disabled */
isDisabled?: boolean;
/** Whether the radio group is read-only */
isReadOnly?: boolean;
/** Whether the radio group is required */
isRequired?: boolean;
/** Validation behavior for the radio group */
validationBehavior?: 'aria' | 'native';
/** Handler called when the radio group receives focus */
onFocus?: (e: FocusEvent) => void;
/** Handler called when the radio group loses focus */
onBlur?: (e: FocusEvent) => void;
/** Handler called when the focus changes within the radio group */
onFocusChange?: (isFocused: boolean) => void;
}
interface RadioGroupAria extends ValidationResult {
/** Props for the radio group wrapper element */
radioGroupProps: DOMAttributes;
/** Props for the radio group's visible label (if any) */
labelProps: DOMAttributes;
/** Props for the radio group description element, if any */
descriptionProps: DOMAttributes;
/** Props for the radio group error message element, if any */
errorMessageProps: DOMAttributes;
}Usage Example:
import { useRadioGroup } from "@react-aria/radio";
import { useRadioGroupState } from "@react-stately/radio";
function RadioGroup({ label, children, ...props }) {
const state = useRadioGroupState(props);
const {
radioGroupProps,
labelProps,
descriptionProps,
errorMessageProps,
isInvalid,
validationErrors
} = useRadioGroup(props, state);
return (
<div {...radioGroupProps}>
<span {...labelProps}>{label}</span>
{props.description && <div {...descriptionProps}>{props.description}</div>}
{children}
{isInvalid && <div {...errorMessageProps}>{validationErrors.join(' ')}</div>}
</div>
);
}Provides behavior and accessibility implementation for individual radio buttons within a radio group.
/**
* Provides the behavior and accessibility implementation for an individual radio button.
* @param props - Props for the radio
* @param state - State for the radio group, as returned by useRadioGroupState
* @param ref - Ref to the HTML input element
* @returns RadioAria object with props and state
*/
function useRadio(props: AriaRadioProps, state: RadioGroupState, ref: RefObject<HTMLInputElement | null>): RadioAria;
interface AriaRadioProps extends RadioProps, DOMProps, AriaLabelingProps, PressEvents {
/** The value of the radio button, used when submitting an HTML form */
value: string;
/** The label for the radio. Accepts any renderable node */
children?: ReactNode;
/** Whether the radio button is disabled */
isDisabled?: boolean;
/** Handler called when the radio is pressed */
onPress?: (e: PressEvent) => void;
/** Handler called when a press interaction starts */
onPressStart?: (e: PressEvent) => void;
/** Handler called when a press interaction ends */
onPressEnd?: (e: PressEvent) => void;
/** Handler called when the press state changes */
onPressChange?: (isPressed: boolean) => void;
/** Handler called when a press is released over the target */
onPressUp?: (e: PressEvent) => void;
/** Handler called when the element is clicked */
onClick?: (e: MouseEvent) => void;
}
interface RadioAria {
/** Props for the label wrapper element */
labelProps: LabelHTMLAttributes<HTMLLabelElement>;
/** Props for the input element */
inputProps: InputHTMLAttributes<HTMLInputElement>;
/** Whether the radio is disabled */
isDisabled: boolean;
/** Whether the radio is currently selected */
isSelected: boolean;
/** Whether the radio is in a pressed state */
isPressed: boolean;
}Usage Example:
import { useRadio } from "@react-aria/radio";
import { useRef } from "react";
function Radio({ children, ...props }) {
const ref = useRef();
const { inputProps, labelProps, isSelected, isDisabled, isPressed } = useRadio(props, state, ref);
return (
<label
{...labelProps}
className={`radio ${isSelected ? 'selected' : ''} ${isDisabled ? 'disabled' : ''}`}
>
<input {...inputProps} ref={ref} />
<span className="radio-indicator" />
{children}
</label>
);
}/** Layout orientation for radio group */
type Orientation = 'horizontal' | 'vertical';
/** React node type */
type ReactNode = React.ReactNode;
/** Mouse event type */
type MouseEvent = React.MouseEvent;
/** State management for radio groups from @react-stately/radio */
interface RadioGroupState {
/** Currently selected value */
selectedValue: string | null;
/** Default selected value */
defaultSelectedValue: string | null;
/** Last focused value for keyboard navigation */
lastFocusedValue: string | null;
/** Whether the radio group is disabled */
isDisabled: boolean;
/** Whether the radio group is read-only */
isReadOnly: boolean;
/** Whether selection is required */
isRequired: boolean;
/** Whether the radio group is invalid */
isInvalid: boolean;
/** Current validation state */
displayValidation: ValidationResult;
/** Set the selected value */
setSelectedValue: (value: string) => void;
/** Set the last focused value */
setLastFocusedValue: (value: string | null) => void;
}
/** Base props for radio groups */
interface RadioGroupProps {
/** Currently selected value */
value?: string | null;
/** Default selected value for uncontrolled components */
defaultValue?: string | null;
/** Handler called when selection changes */
onChange?: (value: string) => void;
}
/** Base props for individual radio buttons */
interface RadioProps {
/** The value of the radio button */
value: string;
/** The label content */
children?: ReactNode;
/** Whether the radio is disabled */
isDisabled?: boolean;
}
/** DOM input properties */
interface InputDOMProps {
/** Form name attribute */
name?: string;
/** Form element */
form?: string;
}
/** Validation properties for ARIA */
interface AriaValidationProps {
/** Whether validation is required */
isRequired?: boolean;
/** Validation behavior */
validationBehavior?: 'aria' | 'native';
/** Custom validation function */
validate?: (value: string | null) => ValidationError | true | null | undefined;
/** Error message */
errorMessage?: string | ((validation: ValidationResult) => string);
}
/** ARIA labeling properties */
interface AriaLabelingProps {
/** Accessible label */
'aria-label'?: string;
/** ID of element that labels this element */
'aria-labelledby'?: string;
/** ID of element that describes this element */
'aria-describedby'?: string;
}
/** DOM properties */
interface DOMProps {
/** Element ID */
id?: string;
}
/** Press event handlers */
interface PressEvents {
/** Handler called when a press interaction starts */
onPressStart?: (e: PressEvent) => void;
/** Handler called when a press interaction ends */
onPressEnd?: (e: PressEvent) => void;
/** Handler called when the press state changes */
onPressChange?: (isPressed: boolean) => void;
/** Handler called when a press is released over the target */
onPressUp?: (e: PressEvent) => void;
/** Handler called when the element is pressed */
onPress?: (e: PressEvent) => void;
}
/** Validation result containing error information */
interface ValidationResult {
/** Whether the input is invalid */
isInvalid: boolean;
/** Array of validation error messages */
validationErrors: string[];
/** Detailed validation information */
validationDetails: ValidationDetails;
}
/** Detailed validation information */
interface ValidationDetails {
/** Validation errors from form constraints */
formErrors: string[];
/** Validation errors from custom validation */
validationErrors: string[];
}
/** Validation error */
interface ValidationError {
/** Error message */
message: string;
}
/** DOM attributes for elements */
interface DOMAttributes {
[key: string]: any;
}
/** Press event information */
interface PressEvent {
/** The type of press event */
type: 'pressstart' | 'pressend' | 'pressup' | 'press';
/** The pointer type that triggered the press event */
pointerType: 'mouse' | 'pen' | 'touch' | 'keyboard' | 'virtual';
/** The target element of the press event */
target: Element;
/** Whether the shift key was held during the press event */
shiftKey: boolean;
/** Whether the ctrl key was held during the press event */
ctrlKey: boolean;
/** Whether the meta key was held during the press event */
metaKey: boolean;
/** Whether the alt key was held during the press event */
altKey: boolean;
}
/** React ref object */
interface RefObject<T> {
readonly current: T | null;
}
/** HTML element attributes */
interface HTMLAttributes<T> {
[key: string]: any;
}
/** HTML label element attributes */
interface LabelHTMLAttributes<T> extends HTMLAttributes<T> {
htmlFor?: string;
}
/** HTML input element attributes */
interface InputHTMLAttributes<T> extends HTMLAttributes<T> {
type?: string;
name?: string;
value?: string | ReadonlyArray<string> | number;
checked?: boolean;
disabled?: boolean;
required?: boolean;
tabIndex?: number;
}The hooks handle validation and error states through the ValidationResult interface:
Common validation scenarios include:
validate prop@react-aria/radio is designed to work in conjunction with @react-stately/radio for state management. This separation allows for flexible state handling while the aria package focuses purely on accessibility and behavior.
Installation:
npm install @react-aria/radio @react-stately/radioBasic state setup:
import { useRadioGroupState, type RadioGroupState } from "@react-stately/radio";
const state = useRadioGroupState({
value: selectedValue,
onChange: setSelectedValue,
defaultValue: 'initial',
isDisabled: false,
isRequired: true
});Required peer dependencies:
@react-stately/radio - Provides RadioGroupState and useRadioGroupState hookreact - React 16.8+ with hooks support (required for hook functionality)react-dom - DOM utilities and event handlingThe package integrates seamlessly with HTML forms and supports both controlled and uncontrolled usage patterns. The aria hooks handle all accessibility concerns while state management is handled separately by the stately package.