# Accessibility (A11Y)

What the Generic CMS Module does — and does not — for accessibility. Operator-focused.

This CMS is mostly server-rendered HTML strings interpolated into the DOM at
runtime. There is no virtual DOM or per-element lifecycle. A11Y is partly
verified by tests and partly by operator manual checks.

## What's covered by tests

### Inputs get a label

The `book` schema's `cover_url` and `buy_url` are URL fields. The
`_editor-list-rows.js` list view sets `aria-label` on the search box:

```html
<input type="search" placeholder="Search..." class="cms-list-search"
       aria-label="Search Book">
```

### Form errors mark the field

`form-bindings.js` adds `cms-invalid` class to URL inputs that fail
`validateUrl`. Combined with screen readers, this gives a hook for the
operator to extend with `aria-invalid="true"`. **Track 6 improvement idea:**
add `aria-invalid` and `aria-describedby` programmatically when the class
is applied. See TRACK-6-A11Y-ARIA.

### Modals have a close button

The delete confirmation modal renders a "Cancel" button. It does **not**
trap focus yet — operators using screen readers may tab out of the modal.

## What's NOT yet enforced by tests

These are operator-side manual checks. They are listed here so reviewers
have a checklist.

- **Focus trap in modals:** operator should manually verify Tab cycles
  inside the delete modal and the import-preview modal.
- **`aria-label` on icon-only buttons:** the dashboard has only-text
  labels right now. If you add icon buttons, label them.
- **Color contrast:** 4.5:1 for body text. Verify with
  [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
  on the colors in `admin.css`.
- **Keyboard navigation across all views:** Tab through dashboard,
  content list, edit form, activity, backups. Every interactive element
  should be reachable and show a visible focus ring.
- **Form error announcements:** when validation fails after submit, the
  status region shows a message. Verify that a screen reader announces it
  (the status div is `data-cms-status`, currently `aria-live="polite"` is
  not yet set).
- **`<table>` semantics in activity view:** confirm `<th scope="col">`
  is used for column headers. Currently plain `<td>` cells only.

## Known gaps (Track 6 follow-ups)

| ID | Gap | Effort |
|---|---|---|
| TRACK-6-A11Y-ARIA | Programmatic `aria-invalid` + `aria-describedby` on form errors | 1h |
| TRACK-6-A11Y-FOCUSTRAP | Focus trap on delete + import modals | 2h |
| TRACK-6-A11Y-LIVE | `aria-live="polite"` on status regions | 30m |
| TRACK-6-A11Y-TABLE | `<th scope>` headers in activity table | 30m |

These are deferred to Track 6. Track 5 ships the posture + checklist,
not the auto-enforcement — script-side A11Y still requires manual review
because most UI is HTML strings rather than DOM components.

## Operator checklist (before going live)

- [ ] Tab through every view. Focus is visible.
- [ ] All form fields have labels (visible text or `aria-label`).
- [ ] Color contrast is at least 4.5:1 for body text.
- [ ] Modal dialogs trap focus while open.
- [ ] Error messages are announced to screen readers.
- [ ] Search input has `aria-label`.
- [ ] Icon-only buttons have `aria-label`.

## See also

- `SECURITY.md` — operator security checklist
- `USAGE.md` — adding content types, using editors
- `cms-doctrine.md` §6 — sanitization at the DOM boundary (adjacent concern)
