---
status: accepted
date: 2026-04-30
decision-makers: R&D Lead
tags: [accessibility, frontend, process]
---

# Accessibility baseline — WCAG 2.2 AA + targeted AAA, Angular CDK + spartan-ng + Tailwind, APF panel testing

## Context and Problem Statement

The host organisation is **APF France Handicap**. Accessibility is the organisation's core mission, not a compliance checkbox. A meaningful proportion of the portal's users will be in situations of disability — visual, motor, cognitive, hearing — and will rely on assistive technologies (screen readers, switch controls, eye tracking, voice control, head pointers, mouth sticks). The portal's accessibility quality is therefore a **product attribute**, not a downstream concern.

The default WCAG 2.2 AA baseline applicable to enterprise portals would _under-serve_ this user base. We need to fix:

- the conformance target (WCAG level + targeted enhancements);
- the implementation stack (component library, primitives, styling);
- the user-preferences surface (contrast, text size, motion, etc.) the portal must offer;
- the testing strategy, including manual testing with users from APF's internal network;
- the CI gates that prevent regression;
- the legal-disclosure surface (the EU Accessibility Act–required accessibility statement).

This ADR fixes the framework. Concrete component-by-component implementation lands as the scaffold and features arrive.

## Decision Drivers

- APF's mission elevates accessibility from "compliance" to "product".
- European Accessibility Act (EAA, in application since June 2025) makes WCAG 2.2 AA a legal floor in the EU; the portal will publish an accessibility statement and accept regression as a defect, not as a backlog item.
- RGAA 4.1 (French national standard, derived from WCAG) is the operational reference for accessibility audits in France; alignment is necessary to be auditable by French experts (including APF's own).
- The user base extends beyond screen-reader users: switch controls, eye tracking, voice control, cognitive load tolerance — all must be considered.
- The technical stack is locked on Angular ([ADR-0004](0004-frontend-stack-angular-csr-zoneless-signals.md)). Component-library choice must therefore be Angular-native or framework-agnostic.
- The dual-audience design ([ADR-0008](0008-identity-model-entra-workforce-dual-audience.md)) means a11y must be uniform across audiences — workforce and (future) customer users alike.
- Anti-bricolage: no hand-rolled a11y where a maintained library exists.

## Considered Options

### Conformance target

- WCAG 2.2 **AA** baseline (EU EAA legal floor).
- WCAG 2.2 **AA + targeted AAA** on criteria with high impact for APF's user base. (Chosen.)
- WCAG 2.2 **AAA** uniform.

### UI component stack

- Angular Material (Angular CDK + Material visual layer).
- **Angular CDK + spartan-ng + Tailwind.** (Chosen — see also `notes/argumentaire-stack-ui-spartan-cdk-tailwind.md` for the team-internal rationale.)
- Custom-on-Angular-CDK only (no spartan-ng).
- React-ecosystem libs (shadcn/ui, dice-ui, animate-ui).

### User-preferences scope

- None (rely on browser/OS prefs).
- `prefers-reduced-motion` only.
- **Full preferences panel** — contrast modes, text size, motion, spacing, simplified UI. (Chosen.)

### Testing strategy

- Automated only (axe-core + lint).
- Automated + external annual audit.
- **Automated + manual screen-reader/keyboard/switch testing + APF user panel cadence + annual internal audit.** (Chosen.)

### Documentation surface

- Internal docs only.
- **Public accessibility statement page (legal EAA requirement) + internal patterns library.** (Chosen.)

## Decision Outcome

### Conformance target

**WCAG 2.2 AA** as the universal baseline. **AAA** applied on the criteria that materially affect APF's user base:

| AAA criterion                         | What it adds vs AA                                                               | Why APF                                    |
| ------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------ |
| **1.4.6 Contrast (Enhanced)**         | 7:1 normal text / 4.5:1 large text (vs 4.5:1 / 3:1 at AA)                        | Low-vision users are a primary audience    |
| **2.2.3 No Timing**                   | No time limits except where essential                                            | Cognitive load and motor speed vary widely |
| **2.3.3 Animation from Interactions** | All interaction-driven animation can be disabled                                 | Vestibular and cognitive disorders         |
| **3.1.5 Reading Level**               | Content readable at lower secondary level _or_ a simpler alternative is provided | Cognitive accessibility                    |
| **1.4.8 Visual Presentation**         | User control over text formatting (line spacing, paragraph spacing, line length) | Dyslexia, low vision                       |
| **2.4.9 Link Purpose (Link Only)**    | Each link's purpose understandable from its text alone (no surrounding context)  | Screen-reader navigation by link list      |
| **3.3.5 Help**                        | Context-sensitive help available where needed                                    | Cognitive accessibility                    |

The remaining AAA criteria (e.g. 1.2.6 Sign Language, 1.2.8 Media Alternative) are **not** uniformly required — they apply where the corresponding content type appears, and are addressed case by case. **RGAA 4.1** alignment is maintained as the audit reference for French experts (APF, CNIL audits, public-sector partners).

### User-preferences panel

The portal exposes a **first-class user-preferences panel** accessible from any page (keyboard-discoverable, screen-reader-announced, persisted in the user's session per [ADR-0010](0010-session-management-redis.md)):

| Preference               | Options                                                                                             | Default                                 |
| ------------------------ | --------------------------------------------------------------------------------------------------- | --------------------------------------- |
| Contrast mode            | Standard (AA-compliant) / Enhanced (AAA, 7:1) / Maximum (high-contrast OS-style, monochrome)        | Standard                                |
| Text size                | 100 % / 125 % / 150 % / 175 % / 200 %                                                               | 100 %                                   |
| Motion                   | Full / Reduced / None                                                                               | inherited from `prefers-reduced-motion` |
| Text spacing             | Default / Generous (line-spacing 1.8, paragraph-spacing 2×, letter-spacing 0.12em)                  | Default                                 |
| Cognitive simplification | Off / On (reduces secondary chrome, larger primary actions, easier-to-read summaries when authored) | Off                                     |
| Reading focus            | Off / Line-by-line                                                                                  | Off                                     |

Preferences are saved per user; an unauthenticated default is read from browser preferences (`prefers-reduced-motion`, `prefers-contrast`, etc.). Changing a preference takes effect immediately, no page reload.

### Component stack

**Angular CDK + TailwindCSS, with spartan-ng-style components written in-house** (the spartan-ng _library_ is deferred — see amendment below). See [`notes/argumentaire-stack-ui-spartan-cdk-tailwind.md`](../../notes/argumentaire-stack-ui-spartan-cdk-tailwind.md) for the original rationale; the design philosophy (headless primitives + Tailwind utility CSS + copy-paste components under our own control) is unchanged. Summary:

> **Amended on 2026-04-30** — Adoption of the spartan-ng package itself is deferred. At the time of scaffolding (2026-04-30), `@spartan-ng/brain` and `@spartan-ng/cli` were still at version `0.0.1-alpha.681`, which trips the project rule against pre-1.0 dependencies. The mitigation is to apply the spartan-ng _philosophy_ (headless primitives on Angular CDK + Tailwind utility CSS + copy-paste components owned in-source) without depending on the lib. Components for v1 are written in-house, in `libs/shared/ui`, on Angular CDK directly. spartan-ng adoption is revisited when the lib reaches `1.0.0`.

- **Angular CDK** (`@angular/cdk`) — primitives accessibility layer maintained by Google's Angular Material team. Equivalent role to Radix UI in React. Provides `Overlay`, `FocusTrap`, `FocusMonitor`, `LiveAnnouncer`, `A11yModule`, `ListKeyManager`, `Drag and Drop`, etc. Battle-tested since 2017.
- ~~**spartan-ng**~~ _(deferred — see amendment above)_. Components are written in-house in `libs/shared/ui/`, on Angular CDK, copy-paste-style. The spartan-ng project is consulted for _design inspiration_ (component patterns, ARIA usage, theming approach) without taking the dependency.
- **Tailwind CSS** — utility-first styling. Tokens for colour, spacing, contrast tiers live in `libs/shared/tokens/`. The contrast tiers map directly to the user preference modes above.

Angular Material is **rejected** because its visual design system (Material Design) is opinionated in ways that clash with both APF branding and the multi-tier contrast requirements. React-ecosystem libraries (shadcn/ui, dice-ui, animate-ui) are rejected as incompatible with the Angular stack ([ADR-0004](0004-frontend-stack-angular-csr-zoneless-signals.md)).

### Concrete patterns to enforce

| Concern         | Rule                                                                                                                                                                                                  |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Sémantique HTML | `<header>`, `<main>`, `<nav>`, `<aside>`, `<footer>` landmarks. One `h1` per page. `<button>` for actions, `<a>` for navigation. **No** `<div onclick>` — flagged by ESLint and rejected at PR review |
| Skip-links      | "Aller au contenu principal" link as the first focusable element on every page                                                                                                                        |
| Focus           | Always visible (no `outline: none` without an at-least-equivalent replacement). Trapped in modals via Angular CDK `FocusTrap`. Restored after navigation/modal close                                  |
| Forms           | Each `<input>` has an associated `<label>`. Errors associated via `aria-describedby`. `autocomplete` attribute correct. Valid HTML5 `type`                                                            |
| Touch targets   | Minimum **44 × 44 CSS pixels** (WCAG 2.5.5 AAA), default 48 × 48 in spartan-ng tokens for safety                                                                                                      |
| Animations      | Respect `prefers-reduced-motion` and the user-preferences `motion` setting. No autoplay video. No carousels with auto-rotation                                                                        |
| Language        | `lang` attribute on `<html>` set dynamically per active locale. Inline `lang` on foreign-language fragments                                                                                           |
| Images          | All non-decorative images carry meaningful `alt`. Decorative images carry `alt=""` and `role="presentation"`                                                                                          |
| ARIA            | Used **only** when native HTML semantics are insufficient. Misuse of ARIA (the most common a11y bug) is treated as a code-quality defect                                                              |
| Error messaging | Programmatic association (`aria-describedby`), live regions (`aria-live="polite"`) for server errors, never just colour to convey state                                                               |

### Tooling and CI gates

| Layer                              | Tool                                                                                                                                                                                            | Where it runs                                                                      | Blocking?                                                                    |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Static lint on Angular templates   | `@angular-eslint/template/*` rules (no-positive-tabindex, click-events-have-key-events, label-has-associated-control, elements-content, no-autofocus, alt-text, valid-aria, button-has-type, …) | local hook + CI (`pnpm ci:check`, [ADR-0015](0015-cicd-gitea-actions.md))          | **Yes**                                                                      |
| Automated runtime a11y check       | `@axe-core/playwright` integrated in e2e tests                                                                                                                                                  | CI (`pnpm ci:quality` — the `a11y` gate of [ADR-0015](0015-cicd-gitea-actions.md)) | **Yes** on `critical` or `serious` violations; warning on `moderate`/`minor` |
| Design-token contrast verification | Custom script that walks `libs/shared/tokens/` colour pairs and verifies WCAG AA + AAA contrast ratios                                                                                          | CI                                                                                 | **Yes**                                                                      |
| Touch-target check                 | Layout test verifying interactive element minimum size                                                                                                                                          | CI (Playwright)                                                                    | **Yes** on min 44 × 44                                                       |
| Motion respect check               | E2E test that turns motion off and verifies no animation occurs                                                                                                                                 | CI (Playwright)                                                                    | **Yes**                                                                      |
| Screen-reader manual testing       | NVDA (Windows), VoiceOver (macOS, iOS), TalkBack (Android)                                                                                                                                      | Manual, before merge of any new component or major refactor                        | **Yes** — checklist in PR template                                           |
| Keyboard-only navigation           | Manual                                                                                                                                                                                          | Manual, before merge of any UI change                                              | **Yes** — checklist in PR template                                           |
| Switch-control / eye-tracking      | Manual, on flagship features                                                                                                                                                                    | APF user panel cadence                                                             | **Yes** as gate for major releases                                           |
| Cognitive walkthrough              | Manual, with user representatives                                                                                                                                                               | APF user panel cadence                                                             | **Yes** as gate for major releases                                           |

### Manual testing — APF user panel

APF France Handicap has, by design, an active network of users in situations of disability. This is a structural advantage that no consultant can replicate.

- **Cadence** : panel-tested before each major release. Composition: at least one user per primary disability category (visual, motor, cognitive, hearing) drawn from the APF network.
- **Scope** : critical user journeys (sign-in, navigation, integrated apps, user-preferences panel, error-recovery flows) plus any new flagship feature.
- **Format** : moderated session, observation-based, recorded with consent. Findings categorised (blocker / serious / moderate / minor) and triaged like security findings — blockers and serious must be fixed before release.
- **Authority** : a finding by a user-panel session is at least as authoritative as an axe-core report. Disagreements escalate to the R&D lead.

### Documentation

- **Accessibility statement page** at `/accessibility` (English) and `/accessibilite` (French) — required by EAA. Content: conformance level claimed, scope (which routes are covered), known issues with target resolution dates, contact email for accessibility complaints, date of last review, RGAA alignment level. Generated from a maintained source-of-truth file in the repo (e.g. `apps/portal-shell/src/accessibility-statement.md`) so the statement evolves with the code, not separately.
- **Internal patterns library** in `docs/accessibility/` (created when first patterns ship) covering: focus management, ARIA usage, form patterns, error messaging, multi-step flows, modals, notifications. Examples drawn from the codebase, kept in sync via review.
- **Component library docs** : every spartan-ng component checked into `libs/shared/ui/` carries a11y notes (keyboard model, ARIA roles emitted, screen-reader expectations).

### Consequences

- Good, because a11y is structural — encoded in the lint, the gates, the components, the tokens, the user-preferences panel — rather than relying on individual discipline.
- Good, because the chosen stack (CDK + spartan-ng + Tailwind) gives full design control without forfeiting Google-tier a11y primitives, and supports the multi-tier contrast/text-size/motion modes without contortion.
- Good, because the AA + targeted AAA approach concentrates effort where APF's user base benefits most, instead of spreading thin across every AAA criterion.
- Good, because access to APF's user network is a competitive advantage — the panel testing is more rigorous than what most enterprise projects can afford.
- Good, because the accessibility statement and the patterns library make the project's a11y posture _legible_ — auditable by external experts, demonstrable to APF stakeholders.
- Bad, because the upfront cost is non-trivial — design tokens with multi-tier contrast, components with theming, user-preferences panel, e2e a11y tests. Estimated +25–35 % of UI delivery time vs. an a11y-as-afterthought approach. Acknowledged and accepted given APF's mission.
- Bad, because spartan-ng is younger than Angular Material — occasional rough edges expected. Mitigated by the copy-paste model: components live in our repo, we own them, fallback is custom-on-CDK without lib dependency.
- Bad, because the user-preferences panel must be wired through every component — components that don't honour the active contrast mode or the active text size break the contract. Enforced by review and a custom lint rule per component.
- Bad, because manual testing cadence (panel sessions, screen-reader, keyboard) is real ops effort. Mitigated by APF's internal network; expense kept inside the org.

### Confirmation

- `libs/shared/tokens` exposes design tokens for colour (across at least three contrast tiers), spacing, typography, motion. Token contrast ratios are verified by a CI script.
- `libs/shared/ui` hosts spartan-ng components, copied in and themed against the tokens. Each component carries an a11y note in its docs.
- Angular CDK is the only allowed source of `Overlay`, `FocusTrap`, `FocusMonitor`, `LiveAnnouncer`, `Drag and Drop`. No equivalent rolled by hand.
- `apps/portal-shell` ships a user-preferences panel persisting to the session (Redis); changes apply without reload; default reads from `prefers-*` browser media queries.
- ESLint configuration enables every `@angular-eslint/template/*` a11y rule. CI fails on any violation.
- `@axe-core/playwright` is integrated in the e2e suite. CI's `a11y` gate fails on any `critical` or `serious` violation, warns on `moderate` or `minor`.
- Design-token contrast verifier and touch-target verifier are part of `pnpm ci:quality`.
- PR template includes a manual-checklist section: keyboard navigation tested, screen reader tested (which one), focus order verified, error states verified.
- Accessibility statement page is published, referenced from the footer, and updated at every major release.
- APF user-panel session findings are tracked as issues with a label `a11y/panel-finding` and triaged with the same gravity as security issues.

## Pros and Cons of the Options

### Conformance target

#### AA + targeted AAA (chosen)

- Good, because matches the user base where it matters and stays achievable on a uniform basis.
- Good, because the AAA criteria selected are the ones with measurable user-impact in APF's primary populations.
- Bad, because the selection is judgement-based — open to debate at audit. Mitigated by documenting the rationale per criterion (this ADR).

#### AA only

- Good, because the EU EAA legal floor.
- Bad, because clearly insufficient given APF's mission. Bare AA on contrast (4.5:1) is uncomfortable for a low-vision user; AA on motion is permissive.

#### AAA uniform

- Good, because the highest WCAG level on every criterion.
- Bad, because some AAA criteria (1.2.6 Sign Language, 1.2.8 Media Alternative, 1.4.9 Images of Text) require content production or content forms we may not have. Pretending uniform AAA when not really delivered is worse than an honest AA + targeted AAA.

### UI component stack

#### Angular CDK + spartan-ng + Tailwind (chosen)

- Good, because Google-tier a11y primitives via CDK + copy-paste components via spartan-ng + utility CSS via Tailwind = full control, no design-system lock-in, no proprietary visuals to fight.
- Good, because aligns with the team-internal preference for the shadcn-style pattern (per dev consultation), without having to break the Angular alignment with NestJS.
- Bad, because spartan-ng is younger (2024). Risk mitigated by the copy-paste model — we own the code.

#### Angular Material

- Good, because most mature, Google-maintained.
- Bad, because the Material Design visual language is opinionated and clashes with multi-tier contrast modes and APF branding. Switching themes within Material is harder than switching themes when the components are owned in source.

#### Custom-on-Angular-CDK only

- Good, because zero dependency beyond Google.
- Bad, because every component (button, dialog, tabs, menu, etc.) must be built from CDK primitives — large upfront cost.

#### React-ecosystem libs

- Bad, because incompatible with Angular ([ADR-0004](0004-frontend-stack-angular-csr-zoneless-signals.md)).

### Testing strategy

#### Automated + manual + APF user panel + annual internal audit (chosen)

- Good, because covers the spectrum from regression detection to genuine usability.
- Good, because user panel access is structurally cheaper than what a consultancy would charge.
- Bad, because requires panel scheduling and triage — operational item.

#### Automated only

- Bad, because tools detect ~30 % of real accessibility defects. The remaining 70 % are interaction patterns invisible to static and runtime checks.

#### Automated + external annual audit

- Good, because external perspective.
- Bad, because expensive (5–15 k€/year for a real audit) and arguably less informed than APF's internal expertise. Reconsidered if APF wants a third-party signature for legal disclosure.

## More Information

- WCAG 2.2 specification: https://www.w3.org/TR/WCAG22/
- WCAG 2.2 quick reference (filterable by level): https://www.w3.org/WAI/WCAG22/quickref/
- European Accessibility Act (EAA): https://ec.europa.eu/social/main.jsp?catId=1202
- RGAA 4.1: https://accessibilite.numerique.gouv.fr/
- APF France Handicap: https://www.apf-francehandicap.org/
- Angular CDK A11y: https://material.angular.io/cdk/a11y/overview
- spartan-ng: https://www.spartan.ng/
- `@angular-eslint`: https://github.com/angular-eslint/angular-eslint
- `@axe-core/playwright`: https://github.com/dequelabs/axe-core-npm/tree/develop/packages/playwright
- Inclusive Components: https://inclusive-components.design/
- Related ADRs: [ADR-0004](0004-frontend-stack-angular-csr-zoneless-signals.md) (Angular stack), [ADR-0010](0010-session-management-redis.md) (preferences persistence), [ADR-0015](0015-cicd-gitea-actions.md) (CI gates `a11y` and `perf`), and the future ADRs for performance budgets, security baseline (ASVS L3 implications cross-cut a11y in some areas — e.g. step-up MFA must remain a11y-conformant), and on-prem infrastructure.
