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

9.8 KiB
Raw Blame History

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

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:

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}:

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