Skip to content

Accessibility (A11Y) Guidelines

This document describes the accessibility standards and practices used in Whity’s web frontend.

Whity’s web frontend is built to meet WCAG 2.1 AA standards, ensuring the application is usable by everyone, including people with disabilities.

  • WCAG 2.1 Level AA: All public and authenticated pages must comply
  • Testing: Automated axe-core scanning + manual keyboard navigation
  • Screen Readers: Tested with NVDA (Windows), VoiceOver (Mac)
  • RTL Support: Full Arabic/RTL keyboard navigation support
  • All interactive elements (buttons, links, inputs) are keyboard accessible
  • Focus order follows visual layout (left-to-right, top-to-bottom)
  • :focus-visible styles provide clear focus indicators (outline or ring)
  • Modal dialogs trap focus within the dialog until dismissed
  • Escape key closes modals and returns focus to the trigger
  • Use proper HTML elements: <button>, <a>, <label>, <input>, <table>, not divs with role attributes
  • Heading hierarchy: H1 → H2 → H3 (no skips)
  • Lists use <ul>, <ol>, <li> elements
  • Tables use <table>, <thead>, <tbody>, <th>, <td> with scope attributes
  • All <input> and <textarea> have associated <label> via htmlFor
  • Error messages linked via aria-describedby
  • aria-invalid="true" when input is invalid
  • aria-label for icon-only buttons
  • aria-current="page" on the current navigation item
  • Skip links use aria-label="Skip to main content"
  • aria-live="polite" on regions that update without navigation
  • aria-live="assertive" for high-priority announcements (rare)
  • role="status" for loading messages, error alerts
  • aria-expanded="true|false" on toggles and collapsibles
  • aria-haspopup="menu|listbox|dialog" on buttons that open overlays
  • aria-owns="id" if the popup isn’t DOM-adjacent

Use the sr-only utility class for content visible only to screen readers:

<span className="sr-only">Current page</span>

The CSS rule (from Tailwind):

.sr-only {
position: absolute;
left: -10000px;
width: 1px;
height: 1px;
overflow: hidden;
}

Use for:

  • Additional context (e.g., “current page” marker on nav)
  • Icon button labels (if no visible text)
  • Status messages in loading states
  • Normal text: 4.5:1 (WCAG AA)
  • Large text (18pt+): 3:1 (WCAG AA)
  • All colors use design tokens, never hardcoded

Check contrast in DevTools Inspect > Styles or use axe DevTools browser extension.

  • All inputs have associated labels
  • Required fields marked with asterisk and aria-required="true"
  • Error messages appear below the input, linked via aria-describedby
  • Submit buttons have clear, descriptive labels (“Save”, “Sign in”, not “Submit”)

Example:

<Input
id="email"
type="email"
label="Email"
required
errorText={errors.email}
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
  • <table> for tabular data (not divs or grids)
  • <caption> or aria-label to identify the table
  • <th scope="col"> for column headers
  • <th scope="row"> for row headers if present
  • Filter rows should have aria-label on their inputs

All interactive elements must be reachable via Tab key. Avoid tabindex > 0 unless necessary.

  • Closes modals, dropdowns, popovers
  • Returns focus to the trigger element
  • Activates buttons and links
  • Submits forms (button with type=“submit”)
  • Navigation within custom widgets (tables, menus, tabs)
  • Usually handled by headless UI libraries (Radix, Headless UI)
  • Tab order follows the visual direction (right-to-left in Arabic mode)
  • Focus indicators work in RTL
  • Logical properties (start/end) instead of left/right where possible
  • Test keyboard nav in RTL mode using dir="rtl" on <html>

Run the accessibility test suite:

Terminal window
npm run test:e2e -- tests/a11y.spec.ts

This scans all major pages for:

  • Missing labels
  • Missing aria attributes
  • Color contrast issues
  • Heading hierarchy
  • Keyboard traps
  • Etc.

Note: Axe can’t test all aspects (e.g., screen reader announcements, focus management edge cases). Manual testing is required.

  1. Keyboard-only navigation (5 min per page):

    • Tab through the entire page
    • Verify all interactive elements are reachable
    • Verify focus is always visible
    • Test Escape key on modals
  2. Screen reader testing:

    • Windows: Narrator (built-in) or NVDA (free)
    • Mac: VoiceOver (built-in)
    • Check form labels are read correctly
    • Check error messages are announced
    • Check button purposes are clear
  3. Contrast checking:

    • DevTools Inspect > Styles > color picker
    • Axe DevTools browser extension
    • Ensure light/dark mode both pass

All components in packages/ui/ are built with a11y in mind:

  • Button: Focus-visible styling, aria-busy for loading, aria-pressed for toggles
  • Input: Label association, aria-invalid, aria-describedby for errors
  • Dialog: Focus trap, Escape closes, focus returns to trigger
  • Tabs: Keyboard nav (arrows, home/end), aria-selected
  • Table: Semantic HTML, scope attributes, sr-only status updates
  • etc.

When creating new components:

  1. Use semantic HTML (<button>, <input>, <label>, etc.)
  2. Include aria-label or visible label
  3. Test keyboard navigation
  4. Add focus-visible styles
  5. Test in automated axe scanner
  6. Test manually with screen reader
  1. Recharts (charting library): Limited native a11y support; we mitigate with aria-label on chart containers
  2. Dynamic content: Some toasts and notifications need manual testing to verify announcements
  3. Color-only indicators: We avoid relying on color alone; always include text or icons

Accessibility is ongoing. When you:

  • Fix a violation, add a test to prevent regression
  • Discover a new pattern, document it here
  • Test with a real user’s screen reader, share findings

For questions about accessibility, open an issue or reach out to the team.