UI Accessibility
Use this skill to produce or review React UI that is semantic, keyboard usable, screen-reader understandable, and touch reachable. Prefer the app's existing design-system primitives. If no wrapper exists, compose mature headless accessible primitives such as Base UI React instead of hand-writing complex ARIA behavior.
Workflow
- Identify the real user task and semantic control before choosing visuals.
- Read the local component wrapper and type definitions before using a primitive.
- Check official Base UI docs when an API, selector, or replacement element is uncertain.
- Implement through public primitives and behavior-level contracts.
- Test with user-perceivable queries and real interaction paths.
Core Rules
- Prefer native semantics: use
button type="button"for actions,a hreffor navigation,button type="submit"for form submission, and real form fields. - Do not use
divorspanas interactive controls unless a headless primitive requires it and already provides role, keyboard, focus, and naming behavior. - Give every interactive control a clear accessible name through visible text,
label,
aria-label,aria-labelledby,alt, or equivalent semantics. - Treat decorative icons as
aria-hidden="true". Put accessible names on icon-only buttons, not on the icon. - Do not make a tooltip the only source of information.
- Preserve controlled and uncontrolled contracts. If a component receives
value, it must receive the matching change handler. Otherwise usedefaultValue. - Style primitive state with exposed data attributes such as
data-open,data-popup-open,data-disabled,data-invalid,data-side, anddata-align; do not mirror primitive state just to add classes. - Keep focus-visible styling obvious. Never remove outline without an equivalent replacement.
- Use true disabled semantics for disabled controls and prevent disabled actions from firing handlers.
Primitive Boundaries
- Use wrapped design-system primitives first. Compose Base UI parts directly only when no local wrapper exists.
- Do not bypass primitives for portals, focus traps, dismissal, keyboard navigation, typeahead, or roving tabindex.
- When using a Base UI
renderprop, keep the replacement element semantically correct. Button-like triggers must render a realbutton type="button". - If a trigger must be non-button, confirm the primitive supports it and apply
the required
nativeButton={false}or equivalent API.
Overlay Selection
- Use Dialog for modal focus management, scroll lock, Escape close, and outside press dismissal.
- Use AlertDialog only for destructive, irreversible, must-confirm decisions.
- Use Drawer for side panels and editor panels. Provide title semantics and a reachable close path.
- Use Popover for click- and touch-reachable explanations, long text, rich layout, interactive content, and infotips.
- Use Tooltip only for short, plain, non-interactive supplemental labels on a trigger that already has an accessible name.
- Use PreviewCard only for hover/focus previews; its popup content must not be the only way touch or screen-reader users can access the information.
- Use Menu or ContextMenu only for action menus, not ordinary form selection.
- Choose Select for closed sets, Combobox for searchable closed sets, and Autocomplete for free input with suggestions.
- Use the primitive's Portal, Positioner, and Popup structure. Fix clipping and stacking context structurally before adding large z-index values.
Forms And Fields
- Use a real
formor Base UI Form for submit flows so Enter submit and submit button behavior remain intact. - Represent each standalone field with Field.Root or equivalent semantics and a
stable
name. - Prefer visible labels. Use visually hidden labels or control-level
aria-labelonly when surrounding context already provides visible meaning. - Attach helper text through Description and errors through Error. Pass invalid
state to the field/control so
aria-invalid, relationships, and styles agree. - Use Fieldset only for grouped controls that form one field, such as checkbox groups, radio groups, multi-thumb sliders, or compound inputs. Every Fieldset needs a Legend.
- Give every checkbox and radio option its own label.
- Let the real group primitive own
value,defaultValue, andonValueChange; Fieldset provides grouping semantics, not state. - Pass external form-library state into primitives explicitly:
name,value,onValueChange,onBlur,invalid, dirty/touched state, and error message. - Mark every non-submit button as
type="button".
Control Choice
- Input: free single-line text.
- Textarea: free multi-line text.
- NumberField: numeric input. Use its parsing, clamping, and stepper behavior instead of hand-written parse and clamp logic.
- Checkbox: independent boolean or one option in a multi-select group.
- Switch: immediate on/off setting with a clear label, not a submit-time choice.
- RadioGroup: mutually exclusive choice.
- CheckboxGroup: multi-select choice.
- Slider: continuous or range values with a label. Give each thumb a distinct accessible name when there are multiple thumbs.
- Tabs: tablist, tab, and tabpanel content switching.
- ToggleGroup or segmented control: mode, filter, or view switching, not tabs.
- Pagination: navigation semantics. Current page, previous, next, and page jump controls must be keyboard usable and clearly named.
- Progress: task progress. Meter: a known-range measurement.
Testing
- Prefer user-perceivable queries:
getByRole(..., { name }), thengetByLabelText,getByPlaceholderText, andgetByText. - Avoid
getByTestIdunless the target has no user semantics, such as canvas, virtualization shims, or third-party boundaries. - Do not mock design-system primitives for business behavior unless the mock preserves real semantics and the primitive itself is not under test.
- Test interactions through realistic user paths: click, keyboard, tab, hover, and type.
- For overlays, assert role/name, open and close behavior, focus behavior, or visible content instead of internal classes.
- Prefer focused integration tests for accessibility regressions over snapshots.
Final Checklist
- Every interactive control has an accessible name.
- The primitive matches the real task, not only the visual shape.
- Tooltip content is short, non-interactive, and non-essential.
- Overlay, select, combobox, and menu behavior is owned by a primitive.
- Form fields have
name, label, description/error relationships, and invalid state. - Icons are either decorative with
aria-hiddenor represented by a named control. - Non-submit buttons explicitly set
type="button". - Controlled and uncontrolled APIs are not mixed.
- State styling uses primitive data attributes.
- Tests find core UI by role, name, label, or visible text.