# Accessibility

AI/UI targets WCAG 2.1 Level AA — keyboard-navigable, focus indicators verified pixel-by-pixel in CI, and safe for screen readers through Shadow DOM.

Every `kai-*` web component is keyboard-operable, carries ARIA semantics that survive the Shadow DOM, and is checked in CI by a real-browser guard that asserts focus indicators actually paint. This page covers what the kit guarantees, what it does not, and what you need to maintain as an integrator.

## Standard targeted

The kit targets **WCAG 2.1 Level AA**. That means:

- **4.5:1** contrast ratio for body text.
- **3:1** for large text and UI components (borders, icons, focus rings).
- All interactive controls reachable and operable without a mouse.

No certification is implied — automated tools can't certify that, and the kit does not currently run an axe-core rule sweep in CI. What *is* gated on every commit is described under [Audit tooling](#audit-tooling) below.

## Shadow DOM and screen readers

`kai-*` web components render inside **Shadow DOM**. Modern screen readers pierce Shadow DOM correctly — accessible names, roles, and live regions propagate without any special workaround in your host page. No extra configuration is required.

## ARIA roles and live regions

- The **message list** carries `role="log"` and `aria-live="polite"`. Streaming tokens are announced without interrupting the reader.
- The **prompt textarea** uses its `placeholder` as an accessible name fallback when no explicit label is associated.
- The **conversation sidebar** is wrapped in a `<nav>` landmark.
- Every **icon-only button** (send, attach, mic, search, copy, like, dislike, regenerate, close) carries an `aria-label` using plain language: "Send message", "Attach file", "Copy message", and so on.

## Keyboard navigation

Every interactive control is reachable by keyboard. **Tab** moves through all focusable controls in DOM order. Focus rings are always visible via `:focus-visible` — they are not hidden on `:focus`.

### Dropdowns and select menus

Applies to `kai-model-switcher`, `kai-scope-picker`, and any similar trigger-menu pairs.

| Key | Action |
|---|---|
| Enter / Space / ↓ | Open the menu |
| ↑ / ↓ | Move between items |
| Home / End | Jump to first / last item |
| Letter key | Jump to first item starting with that letter (typeahead) |
| Enter / Space | Select the focused item |
| Esc | Close without selecting; focus returns to the trigger |
| Tab | Close and move focus past the trigger |

Focus always returns to the trigger after the menu closes — whether by Esc, selection, or a click outside.

### Collapsible panels

Applies to reasoning blocks, tool-call panels, and any other expandable section.

| Key | Action |
|---|---|
| Enter / Space | Toggle expanded / collapsed |
| Tab | Move into the expanded content |
| Esc | Collapse (when focused inside) |

### Entity-pill triggers

Typing a trigger char (`/` for skills, `@` for agents/plugins) in the prompt input opens a caret-anchored menu; selecting an item inserts an atomic pill.

| Key | Action |
|---|---|
| ↑ / ↓ | Navigate menu items |
| Enter / Tab | Insert the focused item as a pill |
| Esc | Close the menu; focus stays in the input |
| ← / → | Move onto an adjacent pill (selects it as one unit); press again to step past |
| Backspace / Delete | Remove the whole pill when it's selected or adjacent |

### Conversation list

| Key | Action |
|---|---|
| ↑ / ↓ | Move between conversations |
| Enter | Select the focused conversation |
| Tab | Move through per-item action buttons |

## Tooltips and hover cards

Tooltips and hover cards (on sources, context meter, attachment previews) conform to **WCAG 2.1 SC 1.4.13 — Content on Hover or Focus**:

- They appear on both **hover and keyboard focus**.
- Moving the pointer onto the tooltip content keeps it visible — it does not vanish when the pointer leaves the trigger.
- **Esc** dismisses the tooltip without moving focus.

## Color contrast and theming

The default light and dark palettes both pass AA contrast requirements. The `theme` attribute on every element (`light | dark | auto`) controls which token set loads.

`auto`, the default, follows the OS `prefers-color-scheme` media query, so users who enable dark mode at the OS level get it automatically.

> **caution:** 
If you override `--color-*` tokens, contrast checking is your responsibility. Nothing in the kit's CI checks contrast, for its own token values or yours.

## Audit tooling

Real-browser guards run in the required CI job, against the **built** bundle in a page with no document-level Tailwind — that is, the way your app loads the kit, not the way Storybook does:

- **Focus-indicator paint guard** (`packages/ui/tests/e2e/focus-ring-paints.spec.ts`) — mounts every element from the generated manifest, walks every focusable control across shadow roots, and screenshots each one blurred and focused to assert the focus indicator changes real pixels. It also fails if any control is invisible yet still reachable by <kbd>Tab</kbd>. It asserts paint rather than CSS classes because a control can carry a perfectly correct `ring-2` class and paint nothing at all inside a shadow root.

> **caution:** 
There is **no axe-core rule sweep in CI**, and the kit does not currently pass one cleanly. A bare mount of every element reports, under the `wcag2a / wcag2aa / wcag21a / wcag21aa` tags, three rules: `button-name` (3 nodes), `label` (1) and `role-img-alt` (1). Widening to axe's **best-practice** rules adds `empty-heading` — `<kai-settings-group>` renders its `<h3>` whether or not it has a label. Which rule set you run matters, so it is named here rather than implied.

Earlier versions of this page claimed an axe audit was "a condition of every release" and reported "zero violations at build time". That was not true: the script it named had no assertions, always exited zero, was wired into no workflow, and needed a server started by hand. It has been removed rather than left standing as false evidence.

Nor does anything check **contrast**, and the focus guard below deliberately cannot: it measures whether pixels *changed*, not whether the result is legible, so a focus ring painted in a colour nobody can see would pass it. Treat contrast and rule conformance as something to verify in your own app.

## What integrators must do

Five things the kit cannot enforce for you:

1. **Do not suppress focus rings.** The kit renders `:focus-visible` rings. Do not apply `outline: none` to `*` or to the web component host in your page CSS.
2. **Verify contrast after token overrides.** Use a tool like the [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) after changing any `--color-*` value.
3. **Write meaningful conversation titles.** The conversation list announces the `title` field to screen readers. "React integration help" is more useful than "Chat 1".
4. **Set a meaningful `placeholder`.** The textarea `placeholder` doubles as an accessible name when no label is associated.
5. **Use `theme="auto"`.** Forcing `theme="light"` on a user who prefers dark mode creates contrast mismatches between the kit and your host page. The default `auto` avoids this.
