Skip to content

Fix accessibility violations on the live site - #26

Merged
drmowinckels merged 3 commits into
mainfrom
a11y-fixes
Oct 16, 2026
Merged

drmowinckels merged 3 commits into
mainfrom
a11y-fixes

Conversation

@drmowinckels

Copy link
Copy Markdown
Contributor

Why this is separate

Cut from main, not from the landing-page work, so it can merge on its own. These defects are live today and none of the fixes depend on any redesign decision. PRs #23, #24 and #25 can rebase onto this and inherit them.

What was found

axe-core 4.14 (WCAG 2.0/2.1/2.2 A + AA, plus best-practice) over every route in the sitemap, both languages, desktop and mobile, including the open mobile menu — 222 violations across 7 types. Now zero, plus 44/44 manual checks.

Fix
Contrast 1.4.3 --color-primary as a text colour is 3.10:1 on dark, 2.68:1 on grey — below AA everywhere it appeared. Text uses now --color-primary-text (5.29/4.58); background and border uses unchanged. --color-light is 4.09:1 on grey, so small text moved to --color-muted. Chroma's monokai code comments were 3.03:1 → github-dark at 6.15:1.
Links in prose 1.4.1 Body links were colour-only at 1.36:1 against surrounding text. Now underlined.
Headings List pages had no h1 at all. Their section title is now the page heading, with cards moving h3 → h2 so levels never skip. Team profiles made the generic "About Member" the only heading and the person's name an h3 — the name is now the h1. A news post started at h3. A titleless section rendered an empty h2.
Landmarks / bypass 1.3.1, 2.4.1 The banner sat outside <main>. There was no skip link at all.
Reflow 1.4.10 Long code lines widened the entire page at 320px instead of scrolling inside their own block.
Empty page /funfacts/ published a page with no content and no heading. It is data for the home-page counters, so it no longer renders as a page — counters and background image unaffected.

The one worth reading about

.reveal starts at opacity: 0 and is only revealed by scroll-reveal.js. All page content was gated on JavaScript running and on the IntersectionObserver firing. It now renders immediately under prefers-reduced-motion, with a <noscript> fallback. This bit me twice while testing — screenshots caught blank sections mid-fade, which is the same failure a crawler, a print stylesheet or a headless renderer would hit.

Test plan

CI builds both languages. What it does not check, and what I ran manually:

  • axe-core over all sitemap routes × 2 languages × 2 viewports + interactive states
  • Skip link is the first tab stop and visible on focus; no keyboard trap; focus indicators on every interactive element
  • prefers-reduced-motion honoured and content still visible without the observer
  • No horizontal scroll at 320px (400% zoom) or 640px (200% zoom)
  • alt text, lang and title on every page

Two honest caveats:

  1. Automated tooling catches roughly a third of real accessibility problems. Zero violations is a floor, not a ceiling — it cannot tell you whether alt text is meaningful or how the site sounds in VoiceOver. A pass with a real screen reader is still worth doing.
  2. I initially recorded a reflow failure on the nav that was an artifact of how I emulated zoom (body { zoom } does not shrink the CSS viewport, so media queries never switch to mobile). Corrected to resize the viewport, which is what browser zoom actually does. The nav is fine; the code blocks genuinely were not.

🤖 Generated with Claude Code

axe-core 4.14 (WCAG 2.0/2.1/2.2 A+AA plus best-practice) over every
route in the sitemap, both languages, desktop and mobile: 222
violations across 7 types. This branch takes it to zero.

Deliberately cut from main rather than from the landing-page work, so
it can ship on its own: these defects are live today and the fixes do
not depend on any redesign decision.

Contrast (WCAG 1.4.3)
  --color-primary as a text colour is 3.10:1 on dark and 2.68:1 on
  grey, below AA everywhere it was used. Text uses now take
  --color-primary-text (5.29 / 4.58); background and border uses are
  unchanged. --color-light is 4.09:1 on grey, so small text moves to
  --color-muted. Chroma's monokai comment colour is 3.03:1 on its own
  background; github-dark clears it at 6.15:1.

Links in prose (WCAG 1.4.1)
  Body-copy links were distinguished by colour alone, at 1.36:1 against
  the surrounding text. Now underlined.

Headings
  List pages had no h1: their section title is now the page heading,
  and cards within move from h3 to h2 so levels never skip. Team
  profiles made the generic "About Member" label the only heading and
  the person's name an h3; the name is now the h1 and the label is no
  longer a heading. A news post started at h3. A section with no title
  rendered an empty h2.

Landmarks and bypass (WCAG 1.3.1, 2.4.1)
  The banner sat outside <main>. There was no skip link at all.

Reflow (WCAG 1.4.10)
  Long code lines widened the whole page at 320px instead of scrolling
  inside their own block.

Empty page
  /funfacts/ published a page with no content and no heading; it is
  data for the home-page counters, so it no longer renders as a page.
  The counters and the background image are unaffected.

Robustness
  .reveal starts at opacity 0 and is shown by scroll-reveal.js, so
  content was gated on JavaScript and on the observer firing. It is now
  visible immediately under prefers-reduced-motion, with a <noscript>
  fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

✅ Preview deployed at https://26--capro-uio.netlify.app
Please check it looks ok before merging.

Adds scripts/a11y and wires it into the preview workflow, so the audit
runs against the deployed preview and fails the job on a regression.
The report is posted as a PR comment.

Two passes: axe-core (WCAG 2.0/2.1/2.2 A+AA plus best-practice) at
desktop and mobile including the nav states that only exist after a
click, then the checks axe cannot make — skip link, keyboard order,
focus indicators, reduced motion, reflow at 320 and 640px, and per-page
lang/title/alt/link labels.

By default it scans one page per layout (each language's home page,
each section landing page, the first child of each section): 30 routes
rather than 122, about 90 seconds. `--all` sweeps the whole sitemap.

Three things the script has to get right, all documented in its README
because each produced a false result while this was being written:
settle animations before sampling colour, or a mid-fade composite
reports contrast failures that do not ship; resize the viewport rather
than setting body{zoom} to emulate zoom, because only the former moves
the CSS viewport and triggers media queries; and skip meta-refresh
stubs before navigating, or axe audits the destination site.

Verified both directions: clean against this branch, and exit 1 with a
correct report against a branch that still has the violations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

✅ Accessibility: no issues

axe-core over 31 routes × 2 viewports (one page per layout), plus 67 manual checks.

All 67 manual checks passed.

Automated tooling catches roughly a third of accessibility problems. A clean run is a floor, not a ceiling.

Both were invisible to the local runs that declared this branch clean,
which is the point of running the audit against the deployed preview.

Leaflet attribution links were #0078a8 on #333 text (2.55:1), under the
3:1 that link-in-text-block requires. Darkening moves them closer to
the surrounding text, so they are underlined instead; the colour itself
is 4.94:1 on the control's white background.

The override has to live outside @layer components. Leaflet's
stylesheet is unlayered and loads after ours, and unlayered styles beat
layered ones regardless of specificity or order, so the first attempt
compiled fine and did nothing. Verified the computed style this time,
not just the generated CSS.

The 404 page's heading used text-primary: 2.68:1 on grey, under the 3:1
large text needs. It surfaced only because a removed route now serves
the 404, so the scanner was auditing it unawares. The 404 is a page
real users land on, so it is now scanned deliberately, and routes that
do not return 200 are skipped rather than silently audited under the
wrong URL.

Also waits for Leaflet to render before scanning: it paints after
networkidle2, so the attribution was sometimes absent and results
flipped between runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@drmowinckels
drmowinckels merged commit 196e23f into main Oct 16, 2026
1 check passed
@drmowinckels
drmowinckels deleted the a11y-fixes branch October 16, 2026 17:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant