Files
intotheeast-com/docs/working/specs/2026-06-20-accessibility-audit-design.md

171 lines
9.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Accessibility Audit Design — intotheeast
**Date:** 2026-06-20
**Standard:** WCAG 2.1 Level AA
**Scope:** All Twig templates in `user/themes/intotheeast/templates/`, CSS tokens, and inline JS
---
## 1. Audit Results
### Failures (must fix)
| ID | Criterion | Severity | Where | Issue |
|----|-----------|----------|-------|-------|
| F1 | 2.4.1 Bypass Blocks | High | Every page | No skip-to-main link — keyboard users must tab through site header on every page load |
| F2 | 1.4.3 Contrast Minimum | High | `tokens.css` | `--color-ink-muted` (#7A7268) is 3.74:1 on `--color-paper` and 3.44:1 on `--color-canvas` — fails 4.5:1 AA for small text. Used for timestamps, location labels, weather spans, and stat labels |
| F3 | 1.4.3 Contrast Minimum | High | `tokens.css` | `--color-accent` (#2A8C73) is 4.30:1 on paper and 3.95:1 on canvas — fails 4.5:1 AA. Used as link text color on journal permalinks, back-pill anchors, and feed-map link |
| F4 | 4.1.2 Name/Role/Value | High | `trip.html.twig` | Filter buttons (`All content` / `Journal` / `Stories`) have no `aria-pressed` — the active filter is communicated only via CSS class, invisible to screen readers |
| F5 | 4.1.2 Name/Role/Value | High | `trip.html.twig` | Stats and Cycling toggle buttons have no `aria-expanded` or `aria-controls` — collapsed/expanded state is invisible to screen readers |
| F6 | 2.1.1 Keyboard | Medium | All three feed templates | Photo strip is scroll-snap only; no keyboard navigation. Slides cannot be advanced by keyboard users |
| F7 | 4.1.2 Name/Role/Value | Medium | `gpx-manager.html.twig` | Each table row has a bare "Delete" button — a screen reader hears "Delete, Delete, Delete" with no way to distinguish which file is targeted |
| F8 | 1.1.1 Non-text Content | Medium | `entry.html.twig` | When the lightbox is open, the enlarged `<img>` has `alt=""` — the displayed photo has no accessible description |
### Passes (no changes needed)
- `<html lang="en">`, `<header>`, `<main>`, `<footer>` landmark structure ✓
- `<nav aria-label="Main navigation">`
- `aria-current="page"` on active nav link ✓
- Global `:focus-visible` rule with `--color-accent` outline ✓
- `prefers-reduced-motion` block covering all animations ✓
- Lightbox: `role="dialog"`, `aria-modal="true"`, `aria-label="Photo viewer"`, labelled close/prev/next buttons ✓
- `aria-hidden="true"` on photo-strip dots, story hero overlay, story scroll cue ✓
- `<time datetime="…">` on all entry dates ✓
- `--color-ink` (#EDE8DF): 14.53:1 on paper ✓
- `--color-ink-2` (#B8B0A4): 8.26:1 on paper ✓
- Story nav title `aria-hidden="true"` (decorative scroll-driven element) ✓
- Back-to-top button `aria-label="Back to top"`
- Hero image `alt` with `hero_alt ?? page.title` fallback ✓
---
## 2. Fixes
### Task 1 — Skip link + main landmark id
**Files:** `user/themes/intotheeast/templates/partials/base.html.twig`, `user/themes/intotheeast/css/style.css`
Add a visually-hidden skip link as the first focusable element in the page, before the site header. On `:focus-visible` it snaps to the top-left corner of the viewport. Add `id="main-content"` to the existing `<main class="site-main">` element so the link has a valid target.
Skip-link CSS: off-screen at rest (e.g. `position: absolute; left: -10000px`), snaps to `top: 0; left: 0` on `:focus-visible`. Styled with accent color to match the site's existing focus ring aesthetic.
### Task 2 — Color token contrast fixes
**Files:** `user/themes/intotheeast/css/tokens.css`
Two token values fail WCAG 1.4.3. Replace both:
| Token | Current | Replacement | Ratio on paper | Ratio on canvas |
|-------|---------|-------------|----------------|-----------------|
| `--color-ink-muted` | #7A7268 | #90887E | 5.07:1 ✓ | 4.66:1 ✓ |
| `--color-accent` | #2A8C73 | #2E9880 | 5.00:1 ✓ | 4.59:1 ✓ |
| `--color-accent-hover` | #236655 | #287A68 | 3.58:1 ✓ (non-text) | — |
`--color-accent-hover` is used only for hover/active states, so the 3:1 non-text contrast criterion (1.4.11) applies rather than 4.5:1. #287A68 passes 3:1.
These are purely token changes — no template or layout changes required.
### Task 3 — ARIA states for filter and toggle buttons
**Files:** `user/themes/intotheeast/templates/trip.html.twig`
**Filter buttons (F4):**
In the template, add `aria-pressed="true"` to the initially-active `All content` button and `aria-pressed="false"` to the other two. In the existing filter JS block (the `trip-filter-btn` click handler), toggle `aria-pressed` alongside `is-active`:
```js
document.querySelectorAll('.trip-filter-btn').forEach(function(btn) {
btn.setAttribute('aria-pressed', btn === activeBtn ? 'true' : 'false');
});
```
**Stats/Cycling toggles (F5):**
Add `aria-expanded="false"` and `aria-controls="trip-stats-block"` to the Stats button. Add `aria-expanded="false"` and `aria-controls="trip-cycling-block"` to the Cycling button. Add the matching `id` attributes to the panels they control (`id="trip-stats-block"` already exists; add `id="trip-cycling-block"` to the cycling panel). In the toggle JS, set `aria-expanded="true"` when the panel is shown, `"false"` when hidden.
No new elements needed — only attribute additions to existing markup and existing JS handlers.
### Task 4 — Photo strip keyboard navigation
**Files:** `user/themes/intotheeast/templates/partials/base.html.twig` (the dot-sync JS IIFE)
For each photo strip with more than one slide, inject a `<button class="strip-prev" aria-label="Previous photo">` and `<button class="strip-next" aria-label="Next photo">` as siblings to the strip after the dots. The buttons are hidden when the strip has only one slide (`data-slides="1"`).
Clicking prev/next scrolls the strip by one slide width via `scrollBy`. The existing dot-sync scroll listener already updates dot state, so dots stay in sync automatically.
The strip container gains `role="region"` and `aria-label="Photo strip"` to group it as a named region for screen reader navigation.
CSS for the buttons: minimal, positioned relative to the strip, styled as teal chevrons matching the site palette. Hidden via `display:none` when `data-slides="1"`.
The strip container itself does NOT get `tabindex="0"` — the injected buttons are the keyboard entry points, which is cleaner than making a scroll container focusable.
### Task 5 — GPX delete button names + lightbox alt text
**Files:** `user/themes/intotheeast/templates/gpx-manager.html.twig`, `user/themes/intotheeast/templates/entry.html.twig`
**GPX delete buttons (F7):**
The delete buttons are built in the JS file-list renderer. Change the button label from `Delete` to `Delete ${f.filename}`:
```js
td.innerHTML = `<button class="gpx-delete" data-filename="${f.filename}">Delete ${f.filename}</button>`;
```
The filename already appears in the adjacent `<td>`, so this adds redundancy for screen readers while not disturbing the visual layout. Alternatively, use `aria-label="Delete ${f.filename}"` and keep the visible text as `Delete` — either approach satisfies 4.1.2.
Use `aria-label`: keeps visible text short (`Delete`), accessible name specific (`Delete 2026-03-25-tokyo.gpx`).
**Lightbox alt text (F8):**
The lightbox open function already copies `data-alt` from the thumbnail. The fix is ensuring `data-alt` is populated with the thumbnail's `alt` attribute (which is `entry.title` — the entry title) and that the full-size `<img>` inside the lightbox receives it on open.
In the existing lightbox open JS: when setting the `src` of the lightbox `<img>`, also set its `alt` from the triggering thumbnail's `alt` attribute.
### Task 6 — axe-core Playwright regression tests
**Files:** `tests/ui/accessibility.spec.js` (new), `package.json`
Add `@axe-core/playwright` as a devDependency. Create `tests/ui/accessibility.spec.js` that runs an axe accessibility scan on the following pages:
- `/` (home)
- `/trips/japan-korea-2026` (trip page, with filter bar and stats)
- `/trips/japan-korea-2026/dailies` (journal feed with map)
- One entry page (use a known demo slug)
- `/trips` (trips archive)
Configuration: fail on `critical` and `serious` violations only. Log `moderate` and `minor` findings as warnings without failing. This matches realistic ongoing CI practice — the fixes in Tasks 15 should bring the site to zero `critical`/`serious` violations.
Each test uses the existing `chromium` project from `playwright.config.js` with the existing auth setup.
---
## 3. What is NOT in scope
- WCAG AAA criteria (e.g. 1.4.6 Enhanced Contrast at 7:1)
- Map marker keyboard navigation — MapLibre GL has built-in keyboard support for map pan/zoom; marker focus is a complex interaction pattern beyond the current scope. Deferred.
- Story page shortcode heading hierarchy — enforcing heading structure in author-written content is a content authoring concern, not a template concern
- `post-form.html.twig` — the form is admin-only and used by Mischa alone; functional accessibility for this page is inherently self-tested
---
## 4. Testing approach
- **Task 15:** Manual verification by loading the page, tabbing through with keyboard, and checking AT output with a screen reader or browser accessibility tree inspector
- **Task 6:** Automated axe-core scan catches regressions after future template changes; run as part of `npx playwright test`
- Playwright tests must load demo data (`make demo-load`) before running, consistent with existing test setup
---
## 5. File map
| File | Changed by |
|------|-----------|
| `user/themes/intotheeast/templates/partials/base.html.twig` | Tasks 1, 4 |
| `user/themes/intotheeast/css/style.css` | Task 1 |
| `user/themes/intotheeast/css/tokens.css` | Task 2 |
| `user/themes/intotheeast/templates/trip.html.twig` | Task 3 |
| `user/themes/intotheeast/templates/gpx-manager.html.twig` | Task 5 |
| `user/themes/intotheeast/templates/entry.html.twig` | Task 5 |
| `tests/ui/accessibility.spec.js` | Task 6 (new) |
| `package.json` | Task 6 |