# WCAG 2.1 AA Accessibility

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 115  
**Tier:** All tiers  
**Depends on:** `foundation-design-system`, `dark-light-theme`  
**Referenced by:** All UI specs

---

## Overview

Zync.is targets WCAG 2.1 Level AA compliance across all tenant-facing and customer-facing interfaces. This spec defines the implementation standards: color contrast, keyboard navigation, ARIA, focus management, skip links, and screen reader support.

---

## Color Contrast Requirements

All text/background combinations must meet 4.5:1 (normal text) or 3:1 (large text ≥ 18px or ≥ 14px bold).

| Token pair | Contrast | Status |
|-----------|----------|--------|
| `--ink` on `--bg` (light) | 14.1:1 | ✓ AAA |
| `--ink` on `--surface` (light) | 12.8:1 | ✓ AAA |
| `--ink-soft` on `--bg` (light) | 9.4:1 | ✓ AA |
| `--accent` on `--bg` (light) | 5.7:1 | ✓ AA |
| `--ink` on `--bg` (dark) | 15.2:1 | ✓ AAA |
| `--accent` on `--bg` (dark) | 9.8:1 | ✓ AAA |
| `--ink-on-accent` on `--accent` | 8.1:1 | ✓ AAA |

`--ink-faint` is NOT compliant for readable text — tokens.css inline comment documents this. Use only for decorative content, disabled controls, and placeholders.

---

## Keyboard Navigation

### Tab Order

All interactive elements (buttons, links, inputs, selects, custom components) must be reachable via Tab in DOM order. Exceptions require `tabindex` annotation in component docs.

Sidebar navigation: Tab moves through items in visual order. Arrow keys optionally navigate within sidebars (`role="navigation"` + `aria-label`).

### Focus Styles

Visible focus indicator required on all interactive elements:

```css
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius);
}
```

Never suppress focus styles globally (`outline: none` without `:focus-visible` alternative = violation).

### Keyboard Shortcuts

| Action | Key |
|--------|-----|
| Skip to main content | Tab (first focus target — skip link) |
| Close modal/sheet | Escape |
| Confirm dialog (where safe) | Enter |
| Navigate list/menu items | Arrow keys |
| Select checkbox | Space |

---

## Skip Links

First focusable element on every page:

```html
<a href="#main-content" class="skip-link">Skip to main content</a>
```

Visible only on focus:

```css
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: var(--accent);
  color: var(--ink-on-accent);
  padding: 8px 16px;
  z-index: var(--z-toast);   /* --z-toast: 400 — defined in spec 114 */
  transition: top 150ms;
}
.skip-link:focus {
  top: 0;
}
```

`id="main-content"` on the `<main>` element.

---

## ARIA Requirements

### Landmark Roles

```html
<header role="banner">   — site header
<nav role="navigation">  — sidebar + top nav (aria-label to distinguish)
<main id="main-content"> — page content
<aside>                  — secondary panels
<footer role="contentinfo">
```

### Modals / Dialogs

```html
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
  <h2 id="modal-title">Create invoice</h2>
  ...
</div>
```

Focus trap: when modal opens, focus moves to first interactive element inside. Tab cycles within modal only. Escape closes. On close, focus returns to trigger element.

### Form Labels

Every input must have an associated `<label>` (explicit `for` + `id`, or `aria-label`, or `aria-labelledby`). Placeholder text is NOT a substitute for labels.

```html
<label for="invoice-number">Invoice number</label>
<input id="invoice-number" type="text" />
```

### Live Regions

Toast notifications use `role="status"` (polite) for success/info and `role="alert"` (assertive) for errors. Announced to screen readers without focus shift.

```html
<div role="status" aria-live="polite" aria-atomic="true">
  Invoice INV-0042 sent successfully.
</div>
```

### Loading States

Async operations: set `aria-busy="true"` on the loading container. Button triggering the action: `aria-disabled="true"` with spinner.

### Kanban Boards

Drag-and-drop cards require keyboard alternative via dnd-kit `KeyboardSensor` — see `tasks-board-engine` spec for implementation details. `aria-grabbed` and `aria-dropeffect` are deprecated in ARIA 1.1; use dnd-kit's built-in `announcements` API for live region announcements instead.

---

## Images and Icons

Decorative icons: `aria-hidden="true"`. Functional icons (icon-only buttons): `aria-label` on the button.

```html
<!-- Icon-only button -->
<button aria-label="Delete invoice INV-0042">
  <Icon name="trash" aria-hidden="true" />
</button>
```

Informational images: `alt` attribute with descriptive text. Decorative images: `alt=""`.

---

## RTL Accessibility

When `dir="rtl"` active (Hebrew locale), reading order matches visual order for screen readers. CSS logical properties (`margin-inline-start`, `padding-inline-end`) ensure correct RTL rendering without duplication. See spec 35 (`rtl-hebrew-ui`).

---

## Testing Requirements

Automated (CI):
- `axe-core` or `@axe-core/playwright` integrated in E2E tests — WCAG 2.1 AA ruleset
- Zero "serious" or "critical" violations allowed to merge

Manual checklist per new UI feature:
- [ ] Keyboard-navigable: reach all interactive elements with Tab
- [ ] Focus visible on all interactive elements
- [ ] Screen reader tested (NVDA/Windows or VoiceOver/macOS)
- [ ] Color contrast verified in both dark and light themes
- [ ] Modal focus trap works (Tab, Shift+Tab, Escape)
- [ ] Form labels present for all inputs
- [ ] Live regions announce toasts

---

## Reduced Motion

Animations must respect `prefers-reduced-motion`:

```css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
```

Add this block to `packages/ui/src/tokens/index.css`. All Tailwind `transition-*` and `animate-*` utilities are automatically silenced by it.

Exceptions where a static fallback must be provided (not just speed-reduced):
- Progress spinners: replace with static percentage text
- Loading skeletons: freeze at first frame (no pulse animation)
- Toast slide-in: appear instantly at final position

---

## Module A11y Checklist

Every new UI module spec must include an `## Accessibility` section covering:

```markdown
## Accessibility

- [ ] All interactive elements keyboard-reachable (Tab/Arrow)
- [ ] Focus visible on all interactive elements
- [ ] ARIA labels on icon-only buttons
- [ ] Form errors associated via `aria-describedby` (see design-system spec)
- [ ] Reduced-motion safe (no essential animations)
- [ ] Color contrast checked in both dark/light themes
- [ ] Screen reader tested or marked "pending SR test" with issue reference
- [ ] Drag-and-drop (if present): keyboard sensor configured (see tasks-board-engine)
- [ ] Charts (if present): aria-label + data table fallback (see reports-analytics)
- [ ] Tiptap editors (if present): Content Security pattern applied (see kb-article-editor)
```

Module specs that omit this section are incomplete and may not be implemented until it is added.

**Additional patterns (add to per-module checklist):**
- [ ] Charts have `role="figure"` + visually-hidden data table alternative
- [ ] Rich text editors (Tiptap): `role="textbox"` `aria-multiline="true"`, toolbar has `role="toolbar"`
- [ ] File upload zones: `role="button"` `tabindex="0"`, keyboard operable, progress announced via `role="progressbar"`
- [ ] Drag-and-drop: `KeyboardSensor` configured, `DndContext` `announcements` prop defined
- [ ] Live-updating counters (timers, live counts): `aria-label` updated periodically; do NOT use `aria-live="assertive"` on counters
- [ ] Data tables with sortable columns: `aria-sort="ascending|descending|none"` on column headers; sort change announced via `aria-live="polite"`
- [ ] Date pickers: full keyboard navigation (Arrow/Page/Home/End), `aria-label` on each day cell

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| WCAG 2.1 AA | Not AAA | AAA requires constraints (no time limits, sign language) beyond a SaaS app's scope; AA is the legal and practical standard |
| `:focus-visible` | Not `:focus` | `:focus` shows rings on mouse clicks (bad UX); `:focus-visible` shows only on keyboard — correct behavior |
| Focus trap in modals | Not allow Tab-out | WCAG 2.1 SC 2.1.2 requires keyboard trap for dialogs — user must be able to close and return |
| Automated axe-core | Not manual-only | Manual testing misses systematic issues; automated catches ~57% of WCAG issues; combined approach covers more |
