WCAG 2.1 AA — release blocker
Button must satisfy these on every render before it ships.
Web (React DOM)
- Renders a real
<button>element withtype="button"by default. aria-busyset automatically whenisLoadingis true.- Focus state is a 2px ring using
--color-ring. Neveroutline: nonewithout a replacement. - Color contrast on every variant verified ≥ 4.5:1 against its background.
- Keyboard:
Tabto focus ·Enter/Spaceto activate. Standard browser behavior; no JS handling needed.
React Native
- Renders
Pressablewith:accessibilityRole="button"accessibilityState={{ disabled, busy: isLoading }}
- Touch target:
lgsize = 44px minimum (Apple HIG / Android Material).
Reduced motion
Transitions limited to duration-short (200ms). When the user has prefers-reduced-motion, transitions are honored by the user agent — no special handling needed.
Screen-reader checks
- The button's text content is its accessible name. If you use only an icon, you must pass an
aria-label(web) oraccessibilityLabel(native). - For
isLoadingstates, prefer keeping the original label rather than swapping to "Loading…" —aria-busycarries the state to assistive tech without a label change.
Color is never the only signal
- Destructive buttons never rely on red alone — pair with the word "Delete" or an icon.
- Disabled state has reduced opacity AND
pointer-events: noneAND the underlying semantic state.
Audit checklist
- Tab-able and visibly focused
- Enter/Space activates
- 4.5:1 contrast on every variant in light AND dark
- 44px touch target on
lgandicon - Loading state announces via
aria-busy(web) oraccessibilityState.busy(native) - Icon-only variant has explicit label