Skip to content

Commit 8fb8d11

Browse files
authored
fix: WCAG AA contrast across public pages + green a11y/Lighthouse CI (#396)
## Why The `playwright-a11y` and Lighthouse `accessibility` CI gates have been red. Root causes (both real, not harness flakes): 1. **Every public page had genuine WCAG 2.1 AA color-contrast violations** — `axe` flagged `color-contrast` (and one `link-in-text-block`) on home, login, register, all legal pages, pricing, about, and the AI/Scholar/Library landings. 2. **The Lighthouse accessibility job aborted before auditing** — it ran `vite build` but never served the output, so Chrome hit a dead `:4173` and failed with `CHROME_INTERSTITIAL_ERROR`. ## What changed Contrast fixed at the **token/source level** so it can't recur page-by-page: - `--sh-muted` `#8a8a8a` → `#6b6b6b` (the dominant muted-text token; was ~3.3:1 on white/paper) - keyboard-hint, legal kicker, footer copyright, dark search-pill text, nav + footer brand wordmark - About roadmap headers → `-text` color variants; About step numerals darkened (were ~1.2:1) + kept `aria-hidden` - Pricing "coming soon" tag → `brand-hover` - data-request in-text email link underlined (WCAG 1.4.1 — a link in body text can't rely on color alone) - the a11y spec now logs the failing **selector + fg/bg/ratio** per node, so a future regression is diagnosable straight from the CI log Lighthouse: config now starts `vite preview` itself (`startServerCommand`) before collecting. ## Verification - All **13/13** a11y smoke pages pass locally (`playwright test tests/a11y.smoke.spec.js`) - Frontend lint clean (0 errors), build succeeds ## Summary by Sourcery Address accessibility contrast issues across public pages and stabilize a11y-related CI by updating design tokens, UI styles, and Lighthouse configuration. Bug Fixes: - Resolve WCAG 2.1 AA color-contrast violations across public pages by adjusting muted text, footer copy, wordmarks, roadmap headers, keyboard hints, and tags to AA-compliant colors. - Ensure in-text email links are visually distinguishable by adding underlines to satisfy WCAG link-in-text requirements. - Fix Lighthouse accessibility CI failures by starting the built app with `vite preview` before running audits so Lighthouse no longer hits a dead port. - Make the About page step numerals decorative-only in the accessibility tree to avoid low-contrast text violations. - Improve a11y smoke test diagnostics by logging failing selectors with their foreground/background colors and contrast ratios. Enhancements: - Refine navigation search text color and About roadmap tone tokens to use more appropriate text-specific variants that preserve brand intent while meeting contrast guidelines. Documentation: - Update the release log with notes about the accessibility contrast fixes and CI gate stabilization. Tests: - Enhance the a11y smoke test output to provide more actionable information for debugging accessibility regressions.
1 parent 96fcf22 commit 8fb8d11

8 files changed

Lines changed: 69 additions & 20 deletions

File tree

docs/release-log.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ internal log into this file when they describe user-visible behavior.
2828

2929
## v2.2.0 — public launch ship (2026-04-30)
3030

31-
### Wave-12.25 — green CI: raise backend test timeout for bcrypt-heavy cases (2026-06-02)
31+
### Wave-12.27 — accessibility contrast fixes + green a11y/Lighthouse CI (2026-06-02)
32+
33+
- Fixed WCAG 2.1 AA color-contrast failures across every public page (home, login, register, all legal pages, pricing, about, AI/Scholar/Library landings). Darkened the muted-text token and the keyboard-hint, legal-kicker, and footer-copy colors; lightened the brand wordmark on the dark nav/footer; switched the About roadmap headers and pricing "coming soon" tag to AA-passing color variants; underlined the in-text email link on the data-request page (links can't rely on color alone). All 13 a11y smoke pages now pass. The a11y test now logs the failing selector + colors so future regressions are diagnosable.
34+
- Fixed the Lighthouse `accessibility` Quality Gate, which had aborted with `CHROME_INTERSTITIAL_ERROR`: the job built the app but never served it, so Lighthouse hit a dead port. The config now starts `vite preview` itself before auditing.
3235

3336
- CI (StudyHub CI + Quality Gates) had been red on `local-main`/#385 because one security test (`recoveryCodes.unit.test.js` — hashes all 10 recovery codes at bcrypt cost 12 and verifies each) took ~5.05s and tripped vitest's 5000ms default. Raised the backend `testTimeout`/`hookTimeout` to 15s in `vitest.config.mjs` (still fails a genuinely-hung test fast). Full backend suite now green on CI (3,513 tests). This was the long-standing red-CI cause, not a regression from recent work.
3437
- Lighthouse CI (`accessibility` Quality Gate) was failing with "module is not defined in ES module scope" — `lighthouse.config.js` used CommonJS `module.exports` in a `"type": "module"` workspace. Renamed it to `lighthouse.config.cjs` and updated the workflow `configPath`. (The `playwright-smoke`/`playwright-a11y` E2E checks remain red on a pre-existing, multi-spec UI/test-drift issue unrelated to this work — tracked separately.)

frontend/studyhub-app/lighthouse.config.cjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
module.exports = {
55
ci: {
66
collect: {
7+
// Serve the built app ourselves. The CI job only runs `vite build`, so
8+
// without this the URL below hit a dead port and Lighthouse aborted with
9+
// CHROME_INTERSTITIAL ("server is not responding"). `vite preview` serves
10+
// dist/ on 4173 (preview.port in vite.config). The action runs from the
11+
// repo root, hence the --prefix.
12+
startServerCommand: 'npm --prefix frontend/studyhub-app run preview',
13+
startServerReadyPattern: 'Local:',
14+
startServerReadyTimeout: 30000,
715
url: ['http://localhost:4173/'],
816
numberOfRuns: 3,
917
settings: {

frontend/studyhub-app/src/components/navbar/Navbar.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ export default function Navbar({
164164
<LogoMark size={28} />
165165
)}
166166
<span style={wordmarkStyle}>
167-
Study<span style={{ color: 'var(--sh-nav-tab-active)' }}>Hub</span>
167+
{/* Brand blue (#2563eb) is only ~3.4:1 on the dark #1a1a1a nav;
168+
the lighter brand-border blue clears AA for the wordmark. */}
169+
Study<span style={{ color: 'var(--sh-brand-border)' }}>Hub</span>
168170
</span>
169171
</Link>
170172

frontend/studyhub-app/src/index.css

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,10 @@
129129
--sh-text: #1a1a1a;
130130
--sh-heading: #111111;
131131
--sh-subtext: #5a5a5a;
132-
--sh-muted: #8a8a8a;
132+
/* #6b6b6b clears WCAG AA (4.5:1) on white, paper (#faf9f7), and warm
133+
paper (#f6f5f2); the prior #8a8a8a was ~3.3:1 and failed the a11y gate
134+
on every muted-text surface. */
135+
--sh-muted: #6b6b6b;
133136

134137
/* ── Brand ───────────────────────────────────────────────────────── */
135138
--sh-brand: #2563eb;
@@ -245,7 +248,9 @@
245248
--sh-footer-dark-bg: #0f172a;
246249
--sh-footer-dark-text: #ffffff;
247250
--sh-footer-dark-muted: rgba(255, 255, 255, 0.46);
248-
--sh-footer-dark-copy: rgba(255, 255, 255, 0.25);
251+
/* 0.62 alpha over the #0f172a footer composites to ~4.7:1; the prior 0.25
252+
rendered ~2.2:1 and failed AA for the footer copyright line. */
253+
--sh-footer-dark-copy: rgba(255, 255, 255, 0.62);
249254

250255
/* ── Extended accent families ──────────────────────────────────── */
251256
--sh-neutral-soft-bg: #f1f5f9;
@@ -411,7 +416,7 @@
411416
--sh-nav-accent: #a0a0a0;
412417
--sh-nav-search-bg: #2a2a2a;
413418
--sh-nav-search-border: #3a3a3a;
414-
--sh-nav-search-text: #6b6b6b;
419+
--sh-nav-search-text: #a8a8a8;
415420
--sh-nav-tab-active: #2563eb;
416421
--sh-nav-badge-bg: #dc2626;
417422

@@ -605,7 +610,7 @@
605610
--sh-nav-accent: #a0a0a0;
606611
--sh-nav-search-bg: #1c1c1c;
607612
--sh-nav-search-border: #2a2a2a;
608-
--sh-nav-search-text: #6b6b6b;
613+
--sh-nav-search-text: #a8a8a8;
609614
--sh-nav-tab-active: #60a5fa;
610615
--sh-nav-badge-bg: #dc2626;
611616

@@ -2822,7 +2827,9 @@ textarea.sh-input {
28222827
}
28232828

28242829
.home-footer-logo-text span {
2825-
color: var(--sh-brand);
2830+
/* Brand blue (#2563eb) is only ~3.4:1 on the dark #0f172a footer; the
2831+
lighter brand-border blue clears AA while keeping the accent. */
2832+
color: var(--sh-brand-border);
28262833
}
28272834

28282835
.home-footer-links {
@@ -3134,7 +3141,9 @@ textarea.sh-input {
31343141
font-weight: 800;
31353142
letter-spacing: 0.08em;
31363143
text-transform: uppercase;
3137-
color: var(--sh-slate-400);
3144+
/* slate-400 (#94a3b8) is ~2.6:1 on white — fails AA. Muted (#6b6b6b)
3145+
reads as a kicker label and clears 4.5:1. */
3146+
color: var(--sh-muted);
31383147
margin-bottom: 12px;
31393148
}
31403149

@@ -3711,7 +3720,8 @@ textarea.sh-input {
37113720
font-size: 10px;
37123721
font-weight: 600;
37133722
font-family: inherit;
3714-
color: #94a3b8;
3723+
/* #94a3b8 on #f1f5f9 is ~2.3:1; slate-600 clears AA for the shortcut hint. */
3724+
color: #475569;
37153725
background: #f1f5f9;
37163726
border: 1px solid #e2e8f0;
37173727
border-radius: 5px;

frontend/studyhub-app/src/pages/legal/AboutPage.jsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,13 @@ const GOAL_TONES = {
7777
},
7878
}
7979

80+
// Use the darker `-text` token variants: the base success (#16a34a) and
81+
// warning (#d97706) are only ~3.2:1 on white and fail AA as heading text.
82+
// The -text variants clear 4.5:1 while staying green/amber.
8083
const ROADMAP_TONES = {
81-
current: 'var(--sh-success)',
82-
next: 'var(--sh-warning)',
83-
future: 'var(--sh-info)',
84+
current: 'var(--sh-success-text)',
85+
next: 'var(--sh-warning-text)',
86+
future: 'var(--sh-info-text)',
8487
}
8588

8689
export default function AboutPage() {
@@ -302,7 +305,12 @@ export default function AboutPage() {
302305
<div style={s.stepsRow}>
303306
{HOW_STEPS.map((step, i) => (
304307
<div key={i} style={s.stepCard}>
305-
<div style={s.stepNum}>{step.step}</div>
308+
{/* Decorative large step numeral — the step's meaning is in
309+
the title/desc below. Faint by design (low contrast), so
310+
hide it from the a11y tree rather than darken it. */}
311+
<div style={s.stepNum} aria-hidden="true">
312+
{step.step}
313+
</div>
306314
<h3 style={s.stepTitle}>{step.title}</h3>
307315
<p style={s.stepDesc}>{step.desc}</p>
308316
</div>
@@ -607,7 +615,9 @@ const s = {
607615
padding: '32px 24px',
608616
textAlign: 'center',
609617
},
610-
stepNum: { fontSize: 40, fontWeight: 'bold', color: 'var(--sh-border)', marginBottom: 16 },
618+
// Muted (not border-grey): the faint #e5e2dd was ~1.2:1 on paper and failed
619+
// even the 3:1 large-text bar. Muted clears it and the numerals stay subtle.
620+
stepNum: { fontSize: 40, fontWeight: 'bold', color: 'var(--sh-muted)', marginBottom: 16 },
611621
stepTitle: { fontSize: 18, fontWeight: 'bold', color: 'var(--sh-heading)', margin: '0 0 12px' },
612622
stepDesc: { fontSize: 14, color: 'var(--sh-muted)', margin: 0, lineHeight: 1.7 },
613623
roadmapGrid: {

frontend/studyhub-app/src/pages/legal/DataRequestPage.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ function DataRequestPage() {
139139
You can also email us directly at{' '}
140140
<a
141141
href={`mailto:${LEGAL_EMAILS.privacy}`}
142-
style={{ color: 'var(--sh-brand)', textDecoration: 'none' }}
142+
// Underlined: a link inside a text block can't rely on color alone
143+
// to be distinguishable (WCAG 1.4.1 / axe link-in-text-block).
144+
style={{ color: 'var(--sh-brand)', textDecoration: 'underline' }}
143145
>
144146
{LEGAL_EMAILS.privacy}
145147
</a>

frontend/studyhub-app/src/pages/pricing/PricingPage.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1559,7 +1559,9 @@ const c = {
15591559
top: 16,
15601560
right: 16,
15611561
background: 'var(--sh-brand-soft)',
1562-
color: 'var(--sh-brand)',
1562+
// brand (#2563eb) on brand-soft (#dbeafe) is 4.23:1 — just under AA;
1563+
// brand-hover (#1d4ed8) clears it.
1564+
color: 'var(--sh-brand-hover)',
15631565
fontSize: 11,
15641566
fontWeight: 700,
15651567
padding: '4px 12px',

frontend/studyhub-app/tests/a11y.smoke.spec.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,22 @@ for (const page of PUBLIC_PAGES) {
6060
console.error(
6161
`[a11y:${page.name}] ${blocking.length} blocking violation(s):\n` +
6262
blocking
63-
.map(
64-
(v) =>
65-
` - ${v.id} (${v.impact}): ${v.help}\n ${v.helpUrl}\n ${v.nodes.length} node(s)`,
66-
)
63+
.map((v) => {
64+
// Print the offending selector + the actual/required contrast
65+
// colors per node. A bare rule name + count isn't actionable
66+
// in CI logs — you can't fix a contrast bug you can't locate.
67+
const nodes = v.nodes
68+
.map((n) => {
69+
const data = n.any?.find((c) => c.id === v.id)?.data
70+
const contrast =
71+
data && data.fgColor
72+
? ` [fg=${data.fgColor} bg=${data.bgColor} ratio=${data.contrastRatio} need=${data.expectedContrastRatio}]`
73+
: ''
74+
return ` ${n.target.join(' ')}${contrast}`
75+
})
76+
.join('\n')
77+
return ` - ${v.id} (${v.impact}): ${v.help}\n ${v.helpUrl}\n ${v.nodes.length} node(s):\n${nodes}`
78+
})
6779
.join('\n'),
6880
)
6981
}

0 commit comments

Comments
 (0)