Skip to content

feat: Support help text in Checkbox, Radio, and Switch#9877

Merged
snowystinger merged 13 commits intomainfrom
checkbox-field
Apr 23, 2026
Merged

feat: Support help text in Checkbox, Radio, and Switch#9877
snowystinger merged 13 commits intomainfrom
checkbox-field

Conversation

@devongovett
Copy link
Copy Markdown
Member

@devongovett devongovett commented Apr 4, 2026

Closes #6192, closes #5548, related to #6151

This adds support for <Text slot="description"> and <FieldError /> in individual checkboxes and switches, and description support for individual radios. Because this requires introducing another wrapper around the <label> elements we currently render, we are deprecating the existing components and introducing CheckboxField + CheckboxButton, RadioField + RadioButton, and SwitchField + SwitchButton. This way additional elements can be added outside the clickable element while still accessing the state (e.g. validation). In the next major we can perhaps rename CheckboxField back to just Checkbox for example.

Switch also now supports isRequired and other validation props like other form components. Although there are not many examples of required switches in the wild, it has been requested in the past, so we're adding it for parity.

Test instructions

Test docs examples for Checkbox, CheckboxGroup, RadioGroup, and Switch in RAC and S2, and components that use these (e.g. Table selection checkbox).

@rspbot
Copy link
Copy Markdown

rspbot commented Apr 4, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented Apr 6, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented Apr 7, 2026

ref: RefCallback<any>
}

export function useSlotId2(initialState: boolean | (() => boolean) = true): SlotAria {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

o dear... haha

@rspbot
Copy link
Copy Markdown

rspbot commented Apr 15, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented Apr 15, 2026

Comment on lines +343 to +344
size: {
size: {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol this made me do a double take

Comment on lines +171 to +173
const radioField = style({
display: 'grid',
gridTemplateColumns: ['max-content', '1fr'],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the field and wrapper styling is quite similar across the Checkbox/RadioGroup/Switch, might be worthwhile reusing/extracting the shared styles so we don't have to remember to update across all 3 places if something changes. That being said, colocation is nice


<ExampleSwitcher>
```tsx render docs={vanillaDocs.exports.CheckboxGroup} links={vanillaDocs.links} props={['label', 'description', 'isDisabled']} initialProps={{label: 'Favorite sports'}} type="vanilla" files={["starters/docs/src/CheckboxGroup.tsx", "starters/docs/src/CheckboxGroup.css"]}
```tsx render docs={vanillaDocs.exports.CheckboxGroup} links={vanillaDocs.links} props={['label', 'description', 'isDisabled']} initialProps={{label: 'Email Notification Preferences'}} type="vanilla" files={["starters/docs/src/CheckboxGroup.tsx", "starters/docs/src/CheckboxGroup.css"]}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that the description added via the control here doesn't get an id attached to it, do we need to add useSlotId2 somewhere else too (useField)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh boy, that looks pre-existing. I think yes we need to migrate from useSlotId to useSlotId2 everywhere, but that's quite a big task and possibly a breaking change given that it now includes a ref that people will need to merge (or be sure they are using mergeProps everywhere). I felt ok with it here since these are new slots but will need to consider it more carefully for existing places. I think we should handle that separately from this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that sounds fine w/ me


export const RadioGroupContext = createContext<ContextValue<RadioGroupProps, HTMLDivElement>>(null);
export const RadioContext = createContext<ContextValue<Partial<RadioProps>, HTMLLabelElement>>(null);
export const RadioFieldContext = createContext<ContextValue<Partial<RadioProps>, HTMLDivElement>>(null);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be RadioFieldProps instead of RadioProps? Noticed something similar in Switch for its FieldContext

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes good catch

@rspbot
Copy link
Copy Markdown

rspbot commented Apr 21, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented Apr 22, 2026

Copy link
Copy Markdown
Member

@LFDanLu LFDanLu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pushed a small change to make the RAC RadioGroup/Switch examples also grey out the help text when disabled, but otherwise looks good to me

@rspbot
Copy link
Copy Markdown

rspbot commented Apr 22, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented Apr 22, 2026

## API Changes

react-aria-components

/react-aria-components:Switch

 Switch {
   aria-controls?: string
   aria-describedby?: string
   aria-details?: string
+  aria-errormessage?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   children?: ChildrenOrFunction<SwitchRenderProps>
   className?: ClassNameOrFunction<SwitchRenderProps> = 'react-aria-Switch'
   defaultSelected?: boolean
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   inputRef?: RefObject<HTMLInputElement | null>
   isDisabled?: boolean
   isReadOnly?: boolean
   isSelected?: boolean
   name?: string
   onBlur?: (FocusEvent<Target>) => void
   onChange?: (boolean) => void
+  onClick?: (MouseEvent<FocusableElement>) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onHoverChange?: (boolean) => void
   onHoverEnd?: (HoverEvent) => void
   onHoverStart?: (HoverEvent) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, SwitchRenderProps>
   slot?: string | null
   style?: StyleOrFunction<SwitchRenderProps>
   value?: string

/react-aria-components:SwitchProps

 SwitchProps {
   aria-controls?: string
   aria-describedby?: string
   aria-details?: string
+  aria-errormessage?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   children?: ChildrenOrFunction<SwitchRenderProps>
   className?: ClassNameOrFunction<SwitchRenderProps> = 'react-aria-Switch'
   defaultSelected?: boolean
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   inputRef?: RefObject<HTMLInputElement | null>
   isDisabled?: boolean
   isReadOnly?: boolean
   isSelected?: boolean
   name?: string
   onBlur?: (FocusEvent<Target>) => void
   onChange?: (boolean) => void
+  onClick?: (MouseEvent<FocusableElement>) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onHoverChange?: (boolean) => void
   onHoverEnd?: (HoverEvent) => void
   onHoverStart?: (HoverEvent) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, SwitchRenderProps>
   slot?: string | null
   style?: StyleOrFunction<SwitchRenderProps>
   value?: string

/react-aria-components:CheckboxField

+CheckboxField {
+  aria-controls?: string
+  aria-describedby?: string
+  aria-details?: string
+  aria-errormessage?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean
+  children?: ChildrenOrFunction<CheckboxFieldRenderProps>
+  className?: ClassNameOrFunction<CheckboxFieldRenderProps> = 'react-aria-CheckboxField'
+  defaultSelected?: boolean
+  excludeFromTabOrder?: boolean
+  form?: string
+  id?: string
+  inputRef?: RefObject<HTMLInputElement | null>
+  isDisabled?: boolean
+  isIndeterminate?: boolean
+  isInvalid?: boolean
+  isReadOnly?: boolean
+  isRequired?: boolean
+  isSelected?: boolean
+  name?: string
+  onBlur?: (FocusEvent<Target>) => void
+  onChange?: (boolean) => void
+  onClick?: (MouseEvent<FocusableElement>) => void
+  onFocus?: (FocusEvent<Target>) => void
+  onFocusChange?: (boolean) => void
+  onKeyDown?: (KeyboardEvent) => void
+  onKeyUp?: (KeyboardEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, CheckboxFieldRenderProps>
+  slot?: string | null
+  style?: StyleOrFunction<CheckboxFieldRenderProps>
+  validate?: (boolean) => ValidationError | boolean | null | undefined
+  validationBehavior?: 'native' | 'aria' = 'native'
+  value?: string
+}

/react-aria-components:CheckboxButton

+CheckboxButton {
+  children?: ChildrenOrFunction<CheckboxButtonRenderProps>
+  className?: ClassNameOrFunction<CheckboxButtonRenderProps> = 'react-aria-CheckboxButton'
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, CheckboxButtonRenderProps>
+  slot?: string | null
+  style?: StyleOrFunction<CheckboxButtonRenderProps>
+}

/react-aria-components:CheckboxFieldContext

+CheckboxFieldContext {
+  UNTYPED
+}

/react-aria-components:RadioField

+RadioField {
+  aria-describedby?: string
+  aria-details?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean
+  children?: ChildrenOrFunction<RadioFieldRenderProps>
+  className?: ClassNameOrFunction<RadioFieldRenderProps> = 'react-aria-RadioField'
+  id?: string
+  inputRef?: RefObject<HTMLInputElement | null>
+  isDisabled?: boolean
+  onBlur?: (FocusEvent<Target>) => void
+  onClick?: (MouseEvent<FocusableElement>) => void
+  onFocus?: (FocusEvent<Target>) => void
+  onFocusChange?: (boolean) => void
+  onKeyDown?: (KeyboardEvent) => void
+  onKeyUp?: (KeyboardEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, RadioFieldRenderProps>
+  slot?: string | null
+  style?: StyleOrFunction<RadioFieldRenderProps>
+  value: string
+}

/react-aria-components:RadioButton

+RadioButton {
+  children?: ChildrenOrFunction<RadioButtonRenderProps>
+  className?: ClassNameOrFunction<RadioButtonRenderProps> = 'react-aria-RadioButton'
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, RadioButtonRenderProps>
+  slot?: string | null
+  style?: StyleOrFunction<RadioButtonRenderProps>
+}

/react-aria-components:RadioFieldContext

+RadioFieldContext {
+  UNTYPED
+}

/react-aria-components:SwitchField

+SwitchField {
+  aria-controls?: string
+  aria-describedby?: string
+  aria-details?: string
+  aria-errormessage?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean
+  children?: ChildrenOrFunction<SwitchFieldRenderProps>
+  className?: ClassNameOrFunction<SwitchFieldRenderProps> = 'react-aria-SwitchField'
+  defaultSelected?: boolean
+  excludeFromTabOrder?: boolean
+  form?: string
+  id?: string
+  inputRef?: RefObject<HTMLInputElement | null>
+  isDisabled?: boolean
+  isInvalid?: boolean
+  isReadOnly?: boolean
+  isRequired?: boolean
+  isSelected?: boolean
+  name?: string
+  onBlur?: (FocusEvent<Target>) => void
+  onChange?: (boolean) => void
+  onClick?: (MouseEvent<FocusableElement>) => void
+  onFocus?: (FocusEvent<Target>) => void
+  onFocusChange?: (boolean) => void
+  onKeyDown?: (KeyboardEvent) => void
+  onKeyUp?: (KeyboardEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, SwitchFieldRenderProps>
+  slot?: string | null
+  style?: StyleOrFunction<SwitchFieldRenderProps>
+  validate?: (boolean) => ValidationError | boolean | null | undefined
+  validationBehavior?: 'native' | 'aria' = 'native'
+  value?: string
+}

/react-aria-components:SwitchButton

+SwitchButton {
+  children?: ChildrenOrFunction<SwitchButtonRenderProps>
+  className?: ClassNameOrFunction<SwitchButtonRenderProps> = 'react-aria-SwitchButton'
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, SwitchButtonRenderProps>
+  slot?: string | null
+  style?: StyleOrFunction<SwitchButtonRenderProps>
+}

/react-aria-components:SwitchFieldContext

+SwitchFieldContext {
+  UNTYPED
+}

/react-aria-components:CheckboxFieldProps

+CheckboxFieldProps {
+  aria-controls?: string
+  aria-describedby?: string
+  aria-details?: string
+  aria-errormessage?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean
+  children?: ChildrenOrFunction<CheckboxFieldRenderProps>
+  className?: ClassNameOrFunction<CheckboxFieldRenderProps> = 'react-aria-CheckboxField'
+  defaultSelected?: boolean
+  excludeFromTabOrder?: boolean
+  form?: string
+  id?: string
+  inputRef?: RefObject<HTMLInputElement | null>
+  isDisabled?: boolean
+  isIndeterminate?: boolean
+  isInvalid?: boolean
+  isReadOnly?: boolean
+  isRequired?: boolean
+  isSelected?: boolean
+  name?: string
+  onBlur?: (FocusEvent<Target>) => void
+  onChange?: (boolean) => void
+  onClick?: (MouseEvent<FocusableElement>) => void
+  onFocus?: (FocusEvent<Target>) => void
+  onFocusChange?: (boolean) => void
+  onKeyDown?: (KeyboardEvent) => void
+  onKeyUp?: (KeyboardEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, CheckboxFieldRenderProps>
+  slot?: string | null
+  style?: StyleOrFunction<CheckboxFieldRenderProps>
+  validate?: (boolean) => ValidationError | boolean | null | undefined
+  validationBehavior?: 'native' | 'aria' = 'native'
+  value?: string
+}

/react-aria-components:CheckboxFieldRenderProps

+CheckboxFieldRenderProps {
+  isDisabled: boolean
+  isIndeterminate: boolean
+  isInvalid: boolean
+  isReadOnly: boolean
+  isRequired: boolean
+  isSelected: boolean
+}

/react-aria-components:CheckboxButtonProps

+CheckboxButtonProps {
+  children?: ChildrenOrFunction<CheckboxButtonRenderProps>
+  className?: ClassNameOrFunction<CheckboxButtonRenderProps> = 'react-aria-CheckboxButton'
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, CheckboxButtonRenderProps>
+  slot?: string | null
+  style?: StyleOrFunction<CheckboxButtonRenderProps>
+}

/react-aria-components:CheckboxButtonRenderProps

+CheckboxButtonRenderProps {
+  isDisabled: boolean
+  isFocusVisible: boolean
+  isFocused: boolean
+  isHovered: boolean
+  isIndeterminate: boolean
+  isInvalid: boolean
+  isPressed: boolean
+  isReadOnly: boolean
+  isRequired: boolean
+  isSelected: boolean
+}

/react-aria-components:RadioFieldProps

+RadioFieldProps {
+  aria-describedby?: string
+  aria-details?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean
+  children?: ChildrenOrFunction<RadioFieldRenderProps>
+  className?: ClassNameOrFunction<RadioFieldRenderProps> = 'react-aria-RadioField'
+  id?: string
+  inputRef?: RefObject<HTMLInputElement | null>
+  isDisabled?: boolean
+  onBlur?: (FocusEvent<Target>) => void
+  onClick?: (MouseEvent<FocusableElement>) => void
+  onFocus?: (FocusEvent<Target>) => void
+  onFocusChange?: (boolean) => void
+  onKeyDown?: (KeyboardEvent) => void
+  onKeyUp?: (KeyboardEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, RadioFieldRenderProps>
+  slot?: string | null
+  style?: StyleOrFunction<RadioFieldRenderProps>
+  value: string
+}

/react-aria-components:RadioFieldRenderProps

+RadioFieldRenderProps {
+  isDisabled: boolean
+  isInvalid: boolean
+  isReadOnly: boolean
+  isRequired: boolean
+  isSelected: boolean
+}

/react-aria-components:RadioButtonProps

+RadioButtonProps {
+  children?: ChildrenOrFunction<RadioButtonRenderProps>
+  className?: ClassNameOrFunction<RadioButtonRenderProps> = 'react-aria-RadioButton'
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, RadioButtonRenderProps>
+  slot?: string | null
+  style?: StyleOrFunction<RadioButtonRenderProps>
+}

/react-aria-components:RadioButtonRenderProps

+RadioButtonRenderProps {
+  isDisabled: boolean
+  isFocusVisible: boolean
+  isFocused: boolean
+  isHovered: boolean
+  isInvalid: boolean
+  isPressed: boolean
+  isReadOnly: boolean
+  isRequired: boolean
+  isSelected: boolean
+}

/react-aria-components:SwitchFieldProps

+SwitchFieldProps {
+  aria-controls?: string
+  aria-describedby?: string
+  aria-details?: string
+  aria-errormessage?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean
+  children?: ChildrenOrFunction<SwitchFieldRenderProps>
+  className?: ClassNameOrFunction<SwitchFieldRenderProps> = 'react-aria-SwitchField'
+  defaultSelected?: boolean
+  excludeFromTabOrder?: boolean
+  form?: string
+  id?: string
+  inputRef?: RefObject<HTMLInputElement | null>
+  isDisabled?: boolean
+  isInvalid?: boolean
+  isReadOnly?: boolean
+  isRequired?: boolean
+  isSelected?: boolean
+  name?: string
+  onBlur?: (FocusEvent<Target>) => void
+  onChange?: (boolean) => void
+  onClick?: (MouseEvent<FocusableElement>) => void
+  onFocus?: (FocusEvent<Target>) => void
+  onFocusChange?: (boolean) => void
+  onKeyDown?: (KeyboardEvent) => void
+  onKeyUp?: (KeyboardEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, SwitchFieldRenderProps>
+  slot?: string | null
+  style?: StyleOrFunction<SwitchFieldRenderProps>
+  validate?: (boolean) => ValidationError | boolean | null | undefined
+  validationBehavior?: 'native' | 'aria' = 'native'
+  value?: string
+}

/react-aria-components:SwitchFieldRenderProps

+SwitchFieldRenderProps {
+  isDisabled: boolean
+  isInvalid: boolean
+  isReadOnly: boolean
+  isRequired: boolean
+  isSelected: boolean
+  state: ToggleState
+}

/react-aria-components:SwitchButtonProps

+SwitchButtonProps {
+  children?: ChildrenOrFunction<SwitchButtonRenderProps>
+  className?: ClassNameOrFunction<SwitchButtonRenderProps> = 'react-aria-SwitchButton'
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, SwitchButtonRenderProps>
+  slot?: string | null
+  style?: StyleOrFunction<SwitchButtonRenderProps>
+}

/react-aria-components:SwitchButtonRenderProps

+SwitchButtonRenderProps {
+  isDisabled: boolean
+  isFocusVisible: boolean
+  isFocused: boolean
+  isHovered: boolean
+  isInvalid: boolean
+  isPressed: boolean
+  isReadOnly: boolean
+  isRequired: boolean
+  isSelected: boolean
+  state: ToggleState
+}

@react-aria/checkbox

/@react-aria/checkbox:CheckboxAria

 CheckboxAria {
+  descriptionProps: DOMAttributesWithRef<HTMLElement>
+  errorMessageProps: DOMAttributesWithRef<HTMLElement>
   inputProps: InputHTMLAttributes<HTMLInputElement>
   isDisabled: boolean
   isInvalid: boolean
   isPressed: boolean
   isSelected: boolean
   labelProps: LabelHTMLAttributes<HTMLLabelElement>
   validationDetails: ValidityState
   validationErrors: Array<string>
 }

@react-aria/radio

/@react-aria/radio:RadioAria

 RadioAria {
+  descriptionProps: DOMAttributesWithRef<HTMLElement>
   inputProps: InputHTMLAttributes<HTMLInputElement>
   isDisabled: boolean
   isPressed: boolean
   isSelected: boolean
 }

@react-aria/switch

/@react-aria/switch:SwitchProps

 SwitchProps {
   autoFocus?: boolean
   children?: ReactNode
   defaultSelected?: boolean
   isDisabled?: boolean
+  isInvalid?: boolean
   isReadOnly?: boolean
+  isRequired?: boolean
   isSelected?: boolean
   onBlur?: (FocusEvent<Target>) => void
   onChange?: (boolean) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
+  validate?: (boolean) => ValidationError | boolean | null | undefined
+  validationBehavior?: 'aria' | 'native' = 'aria'
   value?: string
 }

/@react-aria/switch:AriaSwitchProps

 AriaSwitchProps {
   aria-controls?: string
   aria-describedby?: string
   aria-details?: string
+  aria-errormessage?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   children?: ReactNode
   defaultSelected?: boolean
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   isDisabled?: boolean
+  isInvalid?: boolean
   isReadOnly?: boolean
+  isRequired?: boolean
   isSelected?: boolean
   name?: string
   onBlur?: (FocusEvent<Target>) => void
   onChange?: (boolean) => void
+  onClick?: (MouseEvent<FocusableElement>) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
+  validate?: (boolean) => ValidationError | boolean | null | undefined
+  validationBehavior?: 'aria' | 'native' = 'aria'
   value?: string
 }

/@react-aria/switch:SwitchAria

 SwitchAria {
+  descriptionProps: DOMAttributesWithRef<HTMLElement>
+  errorMessageProps: DOMAttributesWithRef<HTMLElement>
   inputProps: InputHTMLAttributes<HTMLInputElement>
   isDisabled: boolean
+  isInvalid: boolean
   isPressed: boolean
   isReadOnly: boolean
   isSelected: boolean
   labelProps: LabelHTMLAttributes<HTMLLabelElement>
+  validationDetails: ValidityState
+  validationErrors: Array<string>
 }

@react-aria/toggle

/@react-aria/toggle:ToggleAria

 ToggleAria {
+  descriptionProps: DOMAttributesWithRef<HTMLElement>
+  errorMessageProps: DOMAttributesWithRef<HTMLElement>
   inputProps: InputHTMLAttributes<HTMLInputElement>
   isDisabled: boolean
   isInvalid: boolean
   isPressed: boolean
   isReadOnly: boolean
   isSelected: boolean
   labelProps: LabelHTMLAttributes<HTMLLabelElement>
+  validationDetails: ValidityState
+  validationErrors: Array<string>
 }

@react-spectrum/s2

/@react-spectrum/s2:Checkbox

 Checkbox {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-controls?: string
   aria-describedby?: string
   aria-details?: string
   aria-errormessage?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   children?: ReactNode
   defaultSelected?: boolean
+  description?: ReactNode
+  errorMessage?: ReactNode | (ValidationResult) => ReactNode
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   inputRef?: RefObject<HTMLInputElement | null>
   isEmphasized?: boolean
   isIndeterminate?: boolean
   isInvalid?: boolean
   isReadOnly?: boolean
   isRequired?: boolean
   isSelected?: boolean
   name?: string
   onBlur?: (FocusEvent<Target>) => void
   onChange?: (boolean) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onPress?: (PressEvent) => void
   onPressChange?: (boolean) => void
   onPressEnd?: (PressEvent) => void
   onPressStart?: (PressEvent) => void
   onPressUp?: (PressEvent) => void
   size?: 'S' | 'M' | 'L' | 'XL' = 'M'
   slot?: string | null
   styles?: StylesProp
   validate?: (boolean) => ValidationError | boolean | null | undefined
   validationBehavior?: 'native' | 'aria' = 'native'
   value?: string
 }

/@react-spectrum/s2:Radio

 Radio {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   children?: ReactNode
+  description?: ReactNode
   id?: string
   inputRef?: RefObject<HTMLInputElement | null>
   isDisabled?: boolean
   onBlur?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onPress?: (PressEvent) => void
   onPressChange?: (boolean) => void
   onPressEnd?: (PressEvent) => void
   onPressStart?: (PressEvent) => void
   onPressUp?: (PressEvent) => void
   slot?: string | null
   styles?: StylesProp
   value: string
 }

/@react-spectrum/s2:Switch

 Switch {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-controls?: string
   aria-describedby?: string
   aria-details?: string
+  aria-errormessage?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   children?: ReactNode
   defaultSelected?: boolean
+  description?: ReactNode
+  errorMessage?: ReactNode | (ValidationResult) => ReactNode
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   inputRef?: RefObject<HTMLInputElement | null>
   isDisabled?: boolean
   isEmphasized?: boolean
+  isInvalid?: boolean
   isReadOnly?: boolean
+  isRequired?: boolean
   isSelected?: boolean
   name?: string
   onBlur?: (FocusEvent<Target>) => void
   onChange?: (boolean) => void
+  onClick?: (MouseEvent<FocusableElement>) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
   size?: 'S' | 'M' | 'L' | 'XL' = 'M'
   slot?: string | null
   styles?: StylesProp
+  validate?: (boolean) => ValidationError | boolean | null | undefined
+  validationBehavior?: 'native' | 'aria' = 'native'
   value?: string
 }

/@react-spectrum/s2:CheckboxProps

 CheckboxProps {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-controls?: string
   aria-describedby?: string
   aria-details?: string
   aria-errormessage?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   children?: ReactNode
   defaultSelected?: boolean
+  description?: ReactNode
+  errorMessage?: ReactNode | (ValidationResult) => ReactNode
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   inputRef?: RefObject<HTMLInputElement | null>
   isEmphasized?: boolean
   isIndeterminate?: boolean
   isInvalid?: boolean
   isReadOnly?: boolean
   isRequired?: boolean
   isSelected?: boolean
   name?: string
   onBlur?: (FocusEvent<Target>) => void
   onChange?: (boolean) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onPress?: (PressEvent) => void
   onPressChange?: (boolean) => void
   onPressEnd?: (PressEvent) => void
   onPressStart?: (PressEvent) => void
   onPressUp?: (PressEvent) => void
   size?: 'S' | 'M' | 'L' | 'XL' = 'M'
   slot?: string | null
   styles?: StylesProp
   validate?: (boolean) => ValidationError | boolean | null | undefined
   validationBehavior?: 'native' | 'aria' = 'native'
   value?: string
 }

/@react-spectrum/s2:RadioProps

 RadioProps {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   children?: ReactNode
+  description?: ReactNode
   id?: string
   inputRef?: RefObject<HTMLInputElement | null>
   isDisabled?: boolean
   onBlur?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onPress?: (PressEvent) => void
   onPressChange?: (boolean) => void
   onPressEnd?: (PressEvent) => void
   onPressStart?: (PressEvent) => void
   onPressUp?: (PressEvent) => void
   slot?: string | null
   styles?: StylesProp
   value: string
 }

/@react-spectrum/s2:SwitchProps

 SwitchProps {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-controls?: string
   aria-describedby?: string
   aria-details?: string
+  aria-errormessage?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   children?: ReactNode
   defaultSelected?: boolean
+  description?: ReactNode
+  errorMessage?: ReactNode | (ValidationResult) => ReactNode
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   inputRef?: RefObject<HTMLInputElement | null>
   isDisabled?: boolean
   isEmphasized?: boolean
+  isInvalid?: boolean
   isReadOnly?: boolean
+  isRequired?: boolean
   isSelected?: boolean
   name?: string
   onBlur?: (FocusEvent<Target>) => void
   onChange?: (boolean) => void
+  onClick?: (MouseEvent<FocusableElement>) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
   size?: 'S' | 'M' | 'L' | 'XL' = 'M'
   slot?: string | null
   styles?: StylesProp
+  validate?: (boolean) => ValidationError | boolean | null | undefined
+  validationBehavior?: 'native' | 'aria' = 'native'
   value?: string
 }

@react-spectrum/switch

/@react-spectrum/switch:Switch

 Switch {
   UNSAFE_className?: string
   UNSAFE_style?: CSSProperties
   alignSelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'center' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'stretch'>
   aria-controls?: string
   aria-describedby?: string
   aria-details?: string
+  aria-errormessage?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   bottom?: Responsive<DimensionValue>
   children?: ReactNode
   defaultSelected?: boolean
   end?: Responsive<DimensionValue>
   excludeFromTabOrder?: boolean
   flex?: Responsive<string | number | boolean>
   flexBasis?: Responsive<number | string>
   flexGrow?: Responsive<number>
   flexShrink?: Responsive<number>
   form?: string
   gridArea?: Responsive<string>
   gridColumn?: Responsive<string>
   gridColumnEnd?: Responsive<string>
   gridColumnStart?: Responsive<string>
   gridRow?: Responsive<string>
   gridRowEnd?: Responsive<string>
   gridRowStart?: Responsive<string>
   height?: Responsive<DimensionValue>
   id?: string
   isDisabled?: boolean
   isEmphasized?: boolean
   isHidden?: Responsive<boolean>
+  isInvalid?: boolean
   isReadOnly?: boolean
+  isRequired?: boolean
   isSelected?: boolean
   justifySelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'center' | 'left' | 'right' | 'stretch'>
   left?: Responsive<DimensionValue>
   margin?: Responsive<DimensionValue>
   marginBottom?: Responsive<DimensionValue>
   marginEnd?: Responsive<DimensionValue>
   marginStart?: Responsive<DimensionValue>
   marginTop?: Responsive<DimensionValue>
   marginX?: Responsive<DimensionValue>
   marginY?: Responsive<DimensionValue>
   maxHeight?: Responsive<DimensionValue>
   maxWidth?: Responsive<DimensionValue>
   minHeight?: Responsive<DimensionValue>
   minWidth?: Responsive<DimensionValue>
   name?: string
   onBlur?: (FocusEvent<Target>) => void
   onChange?: (boolean) => void
+  onClick?: (MouseEvent<FocusableElement>) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
   order?: Responsive<number>
   position?: Responsive<'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'>
   right?: Responsive<DimensionValue>
   start?: Responsive<DimensionValue>
   top?: Responsive<DimensionValue>
+  validate?: (boolean) => ValidationError | boolean | null | undefined
+  validationBehavior?: 'aria' | 'native' = 'aria'
   value?: string
   width?: Responsive<DimensionValue>
   zIndex?: Responsive<number>
 }

/@react-spectrum/switch:SpectrumSwitchProps

 SpectrumSwitchProps {
   UNSAFE_className?: string
   UNSAFE_style?: CSSProperties
   alignSelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'center' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'stretch'>
   aria-controls?: string
   aria-describedby?: string
   aria-details?: string
+  aria-errormessage?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean
   bottom?: Responsive<DimensionValue>
   children?: ReactNode
   defaultSelected?: boolean
   end?: Responsive<DimensionValue>
   excludeFromTabOrder?: boolean
   flex?: Responsive<string | number | boolean>
   flexBasis?: Responsive<number | string>
   flexGrow?: Responsive<number>
   flexShrink?: Responsive<number>
   form?: string
   gridArea?: Responsive<string>
   gridColumn?: Responsive<string>
   gridColumnEnd?: Responsive<string>
   gridColumnStart?: Responsive<string>
   gridRow?: Responsive<string>
   gridRowEnd?: Responsive<string>
   gridRowStart?: Responsive<string>
   height?: Responsive<DimensionValue>
   id?: string
   isDisabled?: boolean
   isEmphasized?: boolean
   isHidden?: Responsive<boolean>
+  isInvalid?: boolean
   isReadOnly?: boolean
+  isRequired?: boolean
   isSelected?: boolean
   justifySelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'center' | 'left' | 'right' | 'stretch'>
   left?: Responsive<DimensionValue>
   margin?: Responsive<DimensionValue>
   marginBottom?: Responsive<DimensionValue>
   marginEnd?: Responsive<DimensionValue>
   marginStart?: Responsive<DimensionValue>
   marginTop?: Responsive<DimensionValue>
   marginX?: Responsive<DimensionValue>
   marginY?: Responsive<DimensionValue>
   maxHeight?: Responsive<DimensionValue>
   maxWidth?: Responsive<DimensionValue>
   minHeight?: Responsive<DimensionValue>
   minWidth?: Responsive<DimensionValue>
   name?: string
   onBlur?: (FocusEvent<Target>) => void
   onChange?: (boolean) => void
+  onClick?: (MouseEvent<FocusableElement>) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
+  onPress?: (PressEvent) => void
+  onPressChange?: (boolean) => void
+  onPressEnd?: (PressEvent) => void
+  onPressStart?: (PressEvent) => void
+  onPressUp?: (PressEvent) => void
   order?: Responsive<number>
   position?: Responsive<'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'>
   right?: Responsive<DimensionValue>
   start?: Responsive<DimensionValue>
   top?: Responsive<DimensionValue>
+  validate?: (boolean) => ValidationError | boolean | null | undefined
+  validationBehavior?: 'aria' | 'native' = 'aria'
   value?: string
   width?: Responsive<DimensionValue>
   zIndex?: Responsive<number>
 }

@snowystinger snowystinger added this pull request to the merge queue Apr 22, 2026
Merged via the queue into main with commit f23d22f Apr 23, 2026
29 checks passed
@snowystinger snowystinger deleted the checkbox-field branch April 23, 2026 00:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support HelpText/FieldError for standalone Checkbox, Radio, and Switch useSwitch does not expose validation API

4 participants