How to Implement Accessible Forms with CheckboxList, RadioList, and Switch in Astryx
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/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/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
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/packages/core/src/RadioList/RadioList.tsx) and individual items in [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
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/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
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
labelprop to ensure screen readers announce the field purpose for CheckboxList, RadioList, and Switch components. - Use
changeActionfor async operations to automatically enablearia-busystates and loading spinners while promises resolve. - Leverage
disabledMessagewithisDisabledto keep controls focusable viaaria-disabledwhile explaining unavailable states via tooltips. - Apply
statuswithtype: "error"to setaria-invalidand communicate validation errors to assistive technologies. - Reference the
.doc.mjsfiles—CheckboxList.doc.mjs,RadioList.doc.mjs, andSwitch.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/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/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, RadioList.doc.mjs, and Switch.doc.mjs contain the official API references, TypeScript definitions, and accessibility guidance for each component in the facebook/astryx repository.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →