Acme Design System

Accessibility Checklist

WCAG 2.1 AA requirements per component.

WCAG 2.1 AA is the minimum bar for all shipped components. AAA is the target where achievable.

Requirements for All Components

  • Color is never the only means of conveying information
  • All interactive elements reachable by keyboard (Tab, Shift+Tab)
  • Focus indicator always visible — never outline: none without a custom replacement
  • Touch targets minimum 44×44px on mobile
  • Text contrast ≥ 4.5:1 (normal text), ≥ 3:1 (large text ≥18pt or 14pt bold)
  • Non-text UI contrast ≥ 3:1 (borders, icons, focus rings)
  • All images have descriptive alt text or alt="" if purely decorative
  • Page has a logical heading hierarchy (H1 → H2 → H3)

Contrast Ratio Reference

CombinationForegroundBackgroundRatioResult
Primary button text #FFFFFF #1955A6 4.8:1 ✓ Pass
Primary button hover #FFFFFF #0B2F5C 9.2:1 ✓ Pass
Body text #383838 #FFFFFF 9.7:1 ✓ Pass
Helper text #909090 #FFFFFF 3.7:1 ⚠ Note
Error text #C62828 #FFFFFF 5.5:1 ✓ Pass
Dark navbar text #FFFFFF #0B2F5C 9.2:1 ✓ Pass
Brand bg text #1955A6 #E7EFF7 4.2:1 ✓ Pass
Disabled text #B0B0B0 #FFFFFF 2.1:1 ⚠ Note
Helper text and disabled text intentionally fall below 4.5:1 — they are supplemental and non-interactive. This is a deliberate system decision, not a bug.

Button

  • Contrast ≥ 4.5:1 all states — Primary: white on #1955A6 = 4.8:1 ✓
  • Focus ring: 3px #A8C4E4 outside stroke always visible
  • Disabled: use aria-disabled='true' not disabled attr (stays keyboard-focusable)
  • Icon-only buttons: aria-label required
  • Non-button element: add role='button' + keyboard handler
Visual Example

Input

  • Every input has a visible <label> — never use placeholder as label
  • Error state: aria-invalid='true' + aria-describedby → error message
  • Helper text: linked via aria-describedby
  • Required fields: aria-required='true' + visual indicator (not color alone)
  • Focus ring: 2px brand-primary border on :focus-visible
Visual Example
We'll never share your email.
Password must be at least 8 characters.

Checkbox

  • Native <input type='checkbox'> or role='checkbox' + aria-checked
  • Indeterminate state: aria-checked='mixed'
  • Group: wrap in <fieldset> with <legend>
  • Click target minimum 24×24px
Visual Example
Notification preferences

Radio

  • Group wrapped in <fieldset> with <legend> describing the question
  • Use native <input type='radio'> inside the group
  • Arrow keys navigate within group — native browser behavior
  • Group error: aria-invalid on the fieldset
Visual Example
Billing cycle

Toggle

  • role='switch' + aria-checked='true/false'
  • Visible label always present alongside toggle
  • State announced on change via live region or updated aria-label
  • Knob vs track contrast ≥ 3:1
Visual Example
Dark mode On
Email notifications Off

TabBar

  • role='tablist' on container; role='tab' on each tab; role='tabpanel' on content
  • Active tab: aria-selected='true'; all others aria-selected='false'
  • Tab panel: aria-labelledby pointing to its tab id
  • Arrow keys navigate tabs (left/right within tablist)
  • Disabled tab: aria-disabled='true'
Visual Example
Tab panel content for Dashboard

Alert

  • role='alert' for error/warning (live region, assertive announce)
  • role='status' for info/success (live region, polite announce)
  • Dismiss button: aria-label='Dismiss alert'
  • Icon is decorative: aria-hidden='true'
Visual Example
Error: Could not save changes. Please try again.
Success: Your changes have been saved.
Warning: You're approaching your storage limit.

Badge

  • Decorative badges: aria-hidden='true'
  • Meaningful (counts, status): provide text alternative in context
  • Count badges in tabs: tab label should include count, e.g. 'All (128)'
Visual Example
Active Complete Failed Warning Inactive

DataTable

  • Use <table> element with <th scope='col'> for column headers
  • Sortable columns: aria-sort='ascending / descending / none'
  • Table caption or aria-label on the <table>
  • Loading state: aria-busy='true' on the table
  • Empty state: descriptive message in <tbody>
  • Row actions: aria-label includes row context
Visual Example
Name ↑ Status Date
Acme RebrandActiveMar 10
Mobile AppReviewFeb 28

Avatar

  • Decorative: aria-hidden='true' on the entire element
  • Interactive (links to profile): aria-label='View [Name]'s profile'
  • Text initials: aria-hidden='true' on the text element
Visual Example
SK

Card

  • Interactive cards: wrap in <a> or <button>
  • Focus ring visible on interactive card wrapper
  • Images inside: meaningful alt text or alt='' if decorative
Visual Example
Project
Acme Rebrand
Brand identity refresh across all touchpoints.
Active