WCAG 2.1 AA — release blocker
Web (React DOM)
- Renders
<div role="radiogroup" aria-label="Access level">. - Each option is a
<button role="radio" aria-checked={isActive}>. - The group label defaults to
"Access level"— passaria-labelwhen the surrounding context already names the field (e.g. on a per-module row, setaria-label="Access level for Payroll"). - Focus ring: 2 px ring on
focus-visible, mapped to--color-ring. - 4.5:1 contrast verified on every tone × active/inactive × light/dark.
React Native
- Group renders with
accessibilityRole="tablist"(RN has noradiogroup). - Each option is
accessibilityRole="radio"withaccessibilityState={{ selected, disabled }}.
Keyboard
Tabmoves into the group.- Arrow keys are not custom-handled — each radio is independently focusable via
Tab. This is intentional for short option lists (4 items); for longer radio groups consider a custom keyboard model. Enter/Spaceactivates the focused option.
Reduced motion
- Background-colour transitions limited to
duration-short(200 ms). User agents honourprefers-reduced-motion.
Color is never the only signal
- The active option is bold (
font-semibold) — not just a colour shift. A colour-blind user can still parse which level is selected. - Each tone is paired with a distinct label string ("None", "View", "Edit", "Full Access") that disambiguates standalone.
Audit checklist
- Group has a meaningful label (
aria-label/accessibilityLabel). -
Tabreaches every option. - Active option distinguishable without colour (weight + label).
- Disabled state at 50 % opacity AND
disabledattribute on each button.