# How to Implement Accessible Forms with CheckboxList, RadioList, and Switch in Astryx

> Learn to build accessible forms in Astryx using CheckboxList RadioList and Switch. These components offer built-in ARIA attributes and screen-reader support for effortless accessibility.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: how-to-guide
- Published: 2026-07-14

---

**Astryx provides three input-group components—**`CheckboxList`**,** `RadioList`**, and** `Switch`**—that ship with built-in ARIA attributes, optimistic UI support, and screen-reader-friendly labeling to create fully accessible forms without additional wrapper libraries.**

Astryx is an open-source design system developed by Meta (facebook/astryx) that prioritizes accessibility in its core component library. When you implement accessible forms with CheckboxList, RadioList, and Switch in Astryx, you leverage native ARIA support, automatic loading states, and semantic HTML that works with assistive technologies out of the box. These components are located in `packages/core/src/` and follow a consistent field-integration pattern that ensures labels, descriptions, and validation states are always announced to assistive devices.

## CheckboxList for Multi-Choice Selection

The **`CheckboxList`** component renders a group of checkboxes for multi-choice selection scenarios. According to the source code in [[`CheckboxList.tsx`](https://github.com/facebook/astryx/blob/main/CheckboxList.tsx)](https://github.com/facebook/astryx/blob/main/packages/core/src/CheckboxList/CheckboxList.tsx), the component automatically manages collection-mode logic while exposing accessibility props to individual items rendered via `CheckboxListItem`.

### Accessibility Features

The group **`label`** prop is always rendered, ensuring screen readers announce the purpose of the collection immediately. Each `CheckboxListItem` (implemented in [[`CheckboxListItem.tsx`](https://github.com/facebook/astryx/blob/main/CheckboxListItem.tsx)](https://github.com/facebook/astryx/blob/main/packages/core/src/CheckboxList/CheckboxListItem.tsx)) renders a `CheckboxInput` with a native accessible `<input>` element. When you provide a **`changeAction`** prop that returns a Promise, the toggled item receives `aria-busy="true"` and displays a visual spinner while the operation is pending.

If the group is disabled via **`isDisabled`**, the controls remain in the tab sequence using `aria-disabled` rather than the `disabled` attribute, while **`disabledMessage`** displays an explanatory tooltip. Additional props like **`description`**, **`status`**, and **`isLabelHidden`** provide fine-grained control over both visual presentation and ARIA output.

### Implementation Example

```tsx
import {CheckboxList, CheckboxListItem} from '@astryxdesign/core';
import {useState} from 'react';

export function NotificationSettings() {
  const [methods, setMethods] = useState<string[]>(['email']);

  return (
    <CheckboxList
      label="How would you like to receive notifications?"
      description="You can pick any combination."
      value={methods}
      onChange={setMethods}
      changeAction={async (newValues) => {
        await fetch('/api/save-notifications', {
          method: 'POST',
          body: JSON.stringify({methods: newValues}),
        });
      }}
    >
      <CheckboxListItem label="Email" value="email" />
      <CheckboxListItem label="SMS" value="sms" />
      <CheckboxListItem label="Push" value="push" />
    </CheckboxList>
  );
}

```

## RadioList for Single-Choice Selection

The **`RadioList`** component handles single-choice selection using native radio button semantics. The implementation in [[`RadioList.tsx`](https://github.com/facebook/astryx/blob/main/RadioList.tsx)](https://github.com/facebook/astryx/blob/main/packages/core/src/RadioList/RadioList.tsx) and individual items in [[`RadioListItem.tsx`](https://github.com/facebook/astryx/blob/main/RadioListItem.tsx)](https://github.com/facebook/astryx/blob/main/packages/core/src/RadioList/RadioListItem.tsx) ensure that only one option can be selected while maintaining full keyboard navigability.

### Accessibility Features

Like CheckboxList, RadioList requires a **`label`** prop that is always rendered for screen readers. The component accepts **`orientation`** (`vertical` or `horizontal`) which controls the visual layout without affecting the semantic radio group structure or ARIA announcements. The **`value`** prop holds the selected string, and **`onChange`** fires with the new value when the selection changes.

You can provide **`disabledMessage`** to explain why the group is disabled, and the **`status`** prop accepts an `InputStatus` object (`{type, message}`) that adds a colored message box and sets `aria-invalid="true"` when the type is `"error"`.

### Implementation Example

```tsx
import {RadioList, RadioListItem} from '@astryxdesign/core';
import {useState} from 'react';

export function ThemeSelector() {
  const [theme, setTheme] = useState('light');

  return (
    <RadioList
      label="Choose a theme"
      description="Only one option can be active at a time."
      value={theme}
      onChange={setTheme}
      orientation="horizontal"
      status={theme ? undefined : {type: 'error', message: 'Please select a theme'}}
    >
      <RadioListItem label="Light" value="light" />
      <RadioListItem label="Dark" value="dark" />
      <RadioListItem label="System" value="system" />
    </RadioList>
  );
}

```

## Switch for Binary Toggles

The **`Switch`** component provides a binary toggle for immediate-action settings. Located in [[`Switch.tsx`](https://github.com/facebook/astryx/blob/main/Switch.tsx)](https://github.com/facebook/astryx/blob/main/packages/core/src/Switch/Switch.tsx), this component renders a button-based toggle with a draggable thumb that supports async operations and loading states.

### Accessibility Features

The **`label`** prop is mandatory and always associated with the switch via ARIA labeling. The component supports **`labelPosition`** (`start` or `end`) and **`labelSpacing`** (`hug` or `spread`) for visual layout, though these do not affect the accessible name calculation. When **`isLoading`** is true or a **`changeAction`** Promise is pending, a spinner appears inside the thumb and `aria-busy` is set to indicate the state change is in progress.

For disabled states, **`isDisabled`** combined with **`disabledMessage`** provides a tooltip explanation while keeping the switch focusable via `aria-disabled`, ensuring screen reader users understand why the control is unavailable.

### Implementation Example

```tsx
import {Switch} from '@astryxdesign/core';
import {useState} from 'react';

export function AutoSaveToggle() {
  const [autoSave, setAutoSave] = useState(false);
  const [loading, setLoading] = useState(false);

  const handleToggle = async (checked: boolean) => {
    setLoading(true);
    await fetch('/api/auto-save', {
      method: 'POST',
      body: JSON.stringify({enabled: checked}),
    });
    setAutoSave(checked);
    setLoading(false);
  };

  return (
    <Switch
      label="Enable auto-save"
      value={autoSave}
      onChange={setAutoSave}
      changeAction={handleToggle}
      isLoading={loading}
      disabledMessage={autoSave ? undefined : 'Auto-save is unavailable while offline'}
    />
  );
}

```

## Shared Accessibility Patterns

All three components implement a consistent field-integration pattern that standardizes accessible behavior across the Astryx form ecosystem.

### Label and Description

Every component requires a **`label`** prop that is always rendered in the DOM, ensuring assistive technologies can announce the field purpose. The optional **`description`** prop adds supplementary text that is associated with the input via ARIA descriptors, providing context without cluttering the label.

### Status and Validation

The **`status`** prop accepts an `InputStatus` object containing `type` (`"error"` or `"success"`) and `message` string. When `type: "error"` is provided, the component automatically sets `aria-invalid="true"` and associates the error message with the input, ensuring screen readers announce validation failures immediately.

### Disabled State Handling

Rather than removing disabled controls from the tab sequence, Astryx components use **`aria-disabled`** in combination with **`disabledMessage`**. This pattern keeps the control focusable while preventing interaction, allowing screen reader users to navigate to the control and hear the explanation for why it is disabled.

### Optimistic UI with changeAction

When you provide a **`changeAction`** function that returns a `Promise`, the component enters an optimistic UI state. The underlying `<input>` receives `aria-busy="true"`, and visual loading indicators appear automatically. This ensures users with assistive technologies are aware that their selection is being processed, preventing duplicate submissions or confusion about state changes.

## Summary

- **Always provide the required `label` prop** to ensure screen readers announce the field purpose for CheckboxList, RadioList, and Switch components.
- **Use `changeAction` for async operations** to automatically enable `aria-busy` states and loading spinners while promises resolve.
- **Leverage `disabledMessage` with `isDisabled`** to keep controls focusable via `aria-disabled` while explaining unavailable states via tooltips.
- **Apply `status` with `type: "error"`** to set `aria-invalid` and communicate validation errors to assistive technologies.
- **Reference the `.doc.mjs` files**—[`CheckboxList.doc.mjs`](https://github.com/facebook/astryx/blob/main/packages/core/src/CheckboxList/CheckboxList.doc.mjs), [`RadioList.doc.mjs`](https://github.com/facebook/astryx/blob/main/packages/core/src/RadioList/RadioList.doc.mjs), and [`Switch.doc.mjs`](https://github.com/facebook/astryx/blob/main/packages/core/src/Switch/Switch.doc.mjs)—for the official prop definitions and ARIA considerations.

## Frequently Asked Questions

### How does CheckboxList announce loading states to screen readers?

When the **`changeAction`** prop returns a Promise, the specific `CheckboxListItem` being toggled receives `aria-busy="true"` and displays a loading spinner. Screen readers announce the busy state, informing users that their selection is being processed and preventing duplicate interactions while the operation is pending.

### What is the difference between RadioList orientation options for accessibility?

The **`orientation`** prop (`vertical` or `horizontal`) in `RadioList` only affects the visual CSS layout of the radio group. According to the source code in [[`RadioList.tsx`](https://github.com/facebook/astryx/blob/main/RadioList.tsx)](https://github.com/facebook/astryx/blob/main/packages/core/src/RadioList/RadioList.tsx), the semantic structure remains a standard radio group with identical keyboard navigation and screen reader announcements regardless of the layout direction chosen.

### How do I disable a Switch while keeping it focusable for screen readers?

Set **`isDisabled={true}`** and provide a **`disabledMessage`** string. The Switch component in [[`Switch.tsx`](https://github.com/facebook/astryx/blob/main/Switch.tsx)](https://github.com/facebook/astryx/blob/main/packages/core/src/Switch/Switch.tsx) applies `aria-disabled="true"` instead of the HTML `disabled` attribute, keeping the element in the tab sequence. The `disabledMessage` renders as a tooltip that screen readers can access, explaining why the control is currently unavailable.

### Where can I find the official prop definitions and accessibility notes for these components?

The documentation files located at [`CheckboxList.doc.mjs`](https://github.com/facebook/astryx/blob/main/packages/core/src/CheckboxList/CheckboxList.doc.mjs), [`RadioList.doc.mjs`](https://github.com/facebook/astryx/blob/main/packages/core/src/RadioList/RadioList.doc.mjs), and [`Switch.doc.mjs`](https://github.com/facebook/astryx/blob/main/packages/core/src/Switch/Switch.doc.mjs) contain the official API references, TypeScript definitions, and accessibility guidance for each component in the facebook/astryx repository.