# Accessibility

Source: https://docs.qiick.io/externa/accessibility

Externa ships user-first accessibility in the authenticated shell: high contrast and reduce motion in the user menu, OS media queries honored without a toggle, and a skip link into `main#main-content`. Target is **WCAG 2.2 AA** across the admin. From externa-core **1.4.0**, CI enforces it with an axe browser smoke suite, `eslint-plugin-jsx-a11y`, and a theme-token contrast check ([#252](https://github.com/qiick-io/externa-core/issues/252)–[#255](https://github.com/qiick-io/externa-core/issues/255)).

<!-- screenshot omitted -->

> **Related**
> Personal theme (light/dark): [Account settings](https://docs.qiick.io/externa/account-settings). Shell layout: [Architecture](https://docs.qiick.io/externa/architecture).

## User preferences

| Preference        | Effect                                                                                         | Storage                                                 |
| ----------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| **High contrast** | Stronger semantic shadcn tokens (`--foreground`, `--muted-foreground`, `--border`, sidebar, …) | `localStorage` + cookie `accessibility_high_contrast=1` |
| **Reduce motion** | Forces reduced animations/transitions when the OS does not already ask                         | `localStorage` + cookie `accessibility_reduce_motion=1` |

UI: user menu → **Accessibility** submenu (summary `Off` / `N on`), same checkmark pattern as Appearance/Language.

Hook: `resources/js/hooks/use-accessibility-preferences.tsx` (sibling of `useAppearance`). Blade boot via `HandleAppearance` + `app.blade.php` stamps `high-contrast` / `reduce-motion` on `<html>` before paint (cookies are not encrypted — see `bootstrap/app.php`).

## OS media queries (always on)

| Query                            | Behavior                                                             |
| -------------------------------- | -------------------------------------------------------------------- |
| `prefers-reduced-motion: reduce` | Motion kill in CSS — no user toggle required                         |
| `prefers-contrast: more`         | Same token bumps as `.high-contrast` without forcing the menu toggle |

The user **Reduce motion** class only adds force-on when the OS preference is off. High contrast from the OS does not require the menu class.

## Skip link & landmark

Authenticated layouts (`app-sidebar-layout`, `app-header-layout`) and the auth simple layout:

1. `SkipToContent` — first focusable control, visible on `:focus`
2. Target `main#main-content` (`tabIndex={-1}` for programmatic focus)

The sidebar (desktop and mobile sheet) and the header variant render a real `<nav aria-label="Main navigation">`; file-detail and chat asides are labelled landmarks.

## Focus, overlays, and announcements (1.4.0)

- **Focus ring:** every control has a visible, offset, full-opacity focus ring and 3:1 input borders (WCAG 1.4.11 / 2.4.7).
- **Overlays:** Sheet, Drawer, Dialog, and the file detail panel return focus to the control that opened them.
- **Navigation:** after an Inertia page visit, focus moves to `#main-content`.
- **Live regions:** toasts, notifications, the AI final answer, empty-table status, and the settings save bar ("Saving" / "Saved") are announced through polite or assertive live regions.
- **Reduced motion:** targeted rules keep Radix / vaul overlays unmounting correctly while motion is reduced.
- **Language:** `<html lang>` follows the UI locale; content-locale switches carry `lang` and text alternatives for flags.
- Icon-only buttons have translated `a11y.*` labels (en / it / de); decorative icons are hidden from assistive tech.

## Brand colours and contrast

Theme tokens (default qiick teal, light, dark, and high-contrast) pass WCAG 2.2 AA: **4.5:1** for text, **3:1** for UI components, borders, and focus indicators. Primary hover states keep AA contrast in both themes.

A custom brand colour from [Project settings → Appearance](https://docs.qiick.io/externa/project-settings#brand-colour-and-focus-ring-contrast) is used for the focus ring only when it reaches **3:1** against the page background; otherwise the teal ring stays ([#453](https://github.com/qiick-io/externa-core/issues/453)). Status is never shown by colour alone. A low-contrast brand can still weaken other brand-coloured surfaces, so check your colour in light and dark, or use high-contrast mode.

## Automated checks (externa-core CI)

| Gate                     | What it checks                                                                                                                                                                                                                                                                                                     |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Axe browser smoke        | `tests/Browser/A11ySmokeBrowserTest.php` (Pest Browser) fails on critical/serious axe issues on login and key authenticated routes (dashboard, collections, items, item edit, files, users, profile, appearance, project settings, `/ai`) in light, dark, and high-contrast. Required `browser` check on code PRs. |
| `eslint-plugin-jsx-a11y` | Errors on shared React components, warnings on pages                                                                                                                                                                                                                                                               |
| `npm run a11y:contrast`  | Checks the documented theme-token pairs (`docs/a11y-contrast-pairs.md`) for light, dark, high-contrast, and high-contrast dark. Default brand only                                                                                                                                                                 |
| Other browser tests      | Landmarks, focus and announcements, accessibility preferences                                                                                                                                                                                                                                                      |

Manual coverage lives in core `docs/a11y-screen-reader-checklist.md`, and known limits of third-party widgets (TipTap, CodeMirror / markdown editor, Leaflet, react-day-picker, recharts, SortableJS) with their mitigations are listed in `docs/a11y-third-party-limits.md`. A passing axe smoke does not cover those widgets.

**Still open:** font size / dyslexia fonts.

## Source map

| Concern         | Location                                                                                                                                        |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Hook            | `resources/js/hooks/use-accessibility-preferences.tsx`                                                                                          |
| Menu            | `resources/js/components/user-menu-content.tsx`                                                                                                 |
| Skip link       | `resources/js/components/skip-to-content.tsx`                                                                                                   |
| CSS             | `resources/css/app.css` (`.high-contrast`, `@media`)                                                                                            |
| Blade / cookies | `resources/views/app.blade.php`, `HandleAppearance`                                                                                             |
| Tests           | `tests/Feature/AccessibilityPreferencesTest.php`, `tests/Browser/AccessibilityPreferencesBrowserTest.php`, `tests/Browser/A11y*BrowserTest.php` |
| Contrast script | `scripts/a11y-contrast.mjs` (`npm run a11y:contrast`)                                                                                           |
