docs: add WCAG 2.1 AA accessibility audit design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
This commit is contained in:
@@ -0,0 +1,170 @@
|
|||||||
|
# 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 1–5 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 1–5:** 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 |
|
||||||
Reference in New Issue
Block a user