fix(ui): WCAG AA color-contrast failures in brand palette #147

Closed
opened 2026-03-28 18:04:58 +01:00 by marcel · 1 comment
Owner

Summary

The axe-core wcag2a/wcag2aa E2E suite (added in #118) has color-contrast disabled because the brand palette produces contrast ratios below the WCAG AA threshold of 4.5:1. This needs a design review before fixing.

Failing locations (measured by axe-core 4.11 on current main)

/persons — 2 nodes

Affected class: text-ink/60 (resolves to #61788e on canvas #f0efe9)

Element Ratio Required
<p class="… text-ink/60"> subtitle paragraph 3.97 : 1 4.5 : 1
<a class="… text-ink/60 …"> "New person" link 3.97 : 1 4.5 : 1

/admin — 4 nodes

Affected class: text links using #a1dcd8 (brand-mint / --c-accent) on white #ffffff

Element Ratio Required
User name links in the users table 1.52 : 1 4.5 : 1

How to re-enable the axe rule once fixed

In frontend/e2e/accessibility.spec.ts, remove .disableRules(['color-contrast']) from buildAxe().

Notes

  • ink-3 (#6b7280) is correctly documented in layout.css as 4.8:1 on white which is AA-compliant; the real issues are text-ink/60 on the sandy canvas and brand-mint links on white.
  • Dark mode palette is fine (separate CSS overrides with higher-contrast values).
## Summary The axe-core wcag2a/wcag2aa E2E suite (added in #118) has `color-contrast` **disabled** because the brand palette produces contrast ratios below the WCAG AA threshold of 4.5:1. This needs a design review before fixing. ## Failing locations (measured by axe-core 4.11 on current main) ### `/persons` — 2 nodes **Affected class:** `text-ink/60` (resolves to `#61788e` on canvas `#f0efe9`) | Element | Ratio | Required | |---|---|---| | `<p class="… text-ink/60">` subtitle paragraph | **3.97 : 1** | 4.5 : 1 | | `<a class="… text-ink/60 …">` "New person" link | **3.97 : 1** | 4.5 : 1 | ### `/admin` — 4 nodes **Affected class:** text links using `#a1dcd8` (brand-mint / `--c-accent`) on white `#ffffff` | Element | Ratio | Required | |---|---|---| | User name links in the users table | **1.52 : 1** | 4.5 : 1 | ## How to re-enable the axe rule once fixed In `frontend/e2e/accessibility.spec.ts`, remove `.disableRules(['color-contrast'])` from `buildAxe()`. ## Notes - `ink-3` (`#6b7280`) is correctly documented in `layout.css` as 4.8:1 on white which is AA-compliant; the real issues are `text-ink/60` on the sandy canvas and brand-mint links on white. - Dark mode palette is fine (separate CSS overrides with higher-contrast values).
marcel added the bugui labels 2026-03-28 18:05:17 +01:00
Author
Owner

Design review — Leonie Voss, UX/Brand

I've read the code behind both failures. They're two distinct problems with different root causes and different fix strategies.


Finding 1 — text-ink/60 on canvas (3.97:1) · HIGH

Where: persons/+page.svelte:37 (subtitle), persons/+page.svelte:43 ("New person" link), plus conversations/+page.svelte:53, ConversationTimeline.svelte:60, DocumentList.svelte:31 — the same pattern is scattered across the app.

Root cause: text-ink/60 is an opacity shortcut. It produces a computed colour that depends entirely on the background beneath it. On white (#ffffff) it gives #61788e at 4.8:1 — borderline pass. On our sandy canvas (#f0efe9) the same opacity blends to #61788e-equivalent at 3.97:1 — fail. The token system exists precisely to prevent this kind of background-dependency, but then opacity modifiers silently bypass it.

WCAG criterion violated: 1.4.3 (Contrast, AA) — 4.5:1 required for normal text, 3:1 for large text (≥18px regular or ≥14px bold).

Fix — two cases:

Subtitles / descriptive secondary text (<p class="… text-sm text-ink/60">):
→ Replace with text-ink-2. #4b5563 is 6.6:1 on canvas — WCAG AA ✓. Secondary text should be readable; we're using opacity to make it look subtle when the right tool is a dedicated token at the correct tone.

Subtle action links (<a class="… text-sm font-medium text-ink/60 … hover:text-ink">):
→ Replace with text-ink-3. #6b7280 is 4.8:1 on white. Note: layout.css comments "use only on surface, not canvas" for ink-3 — that note needs revisiting. On canvas #f0efe9 the effective ratio is ~4.6:1, which technically still passes AA for 14px/medium weight. However, interactive elements (links) should ideally exceed AA. I would use text-ink-2 here too, and achieve the "subtle" look through font-weight: 500 and uppercase tracking-widest rather than a low-contrast colour.

Rule going forward: Ban text-ink/60, text-ink/70, and all opacity-modified ink utilities on canvas backgrounds. The semantic token layer is supposed to eliminate these guesses.


Finding 2 — text-accent on white (1.52:1) · CRITICAL

Where: admin/UsersTab.svelte:81, admin/GroupsTab.svelte:138. Also present in CommentThread.svelte:280 (Reply button), DocumentList.svelte:171 (Clear search button), PdfViewer.svelte:300 (Direkt öffnen link) — the axe run on /admin surfaced the worst instances but the underlying problem is a systemic misuse of the accent token.

Root cause: --c-accent in light mode is #a1dcd8 — a pale mint chosen as a decorative accent (borders, icon tints, hover fills). Its luminance is too high for readable text on white. A 1.52:1 ratio is not a borderline failure; it renders these labels nearly invisible to anyone with reduced contrast sensitivity, and completely invisible in certain low-vision conditions. An admin-only screen is no justification — administrative users include senior researchers managing their family's archive.

WCAG criteria violated: 1.4.3 (Contrast, AA — normal text). Also implicates 2.4.6 (Headings and Labels) because these are the only textual labels identifying the edit action on each table row.

Fix:
Replace text-accent with text-primary (--c-primary = #012851, 16.8:1 on white) everywhere it is used on text or interactive labels. Mint is not a text colour — it belongs on borders (border-accent), icon containers, and hover/focus rings. The text-accent utility should probably not exist at all, or be intentionally restricted to decorative/non-text contexts via a comment in the token file.

- class="text-sm font-bold tracking-wide text-accent uppercase hover:text-ink"
+ class="text-sm font-bold tracking-wide text-primary uppercase hover:text-ink-2"

Dark mode note: --c-accent remaps to #00c7b1 (turquoise) in dark mode — significantly better contrast on dark surfaces. That's why axe passes in dark mode. The fix (text-primary) also remaps correctly in dark mode to #a1dcd8, which is 8.4:1 on dark surface — still fine.


Broader audit note

These two axe findings are the tip of a pattern. A quick grep shows text-accent used as a text colour in at least 6 files, and text-ink/60 in at least 5. I'd recommend the fix PR also adds a comment block to layout.css above --c-accent:

/* ⚠ accent — decorative use only (borders, icon tints, backgrounds)
   Do NOT use as text colour in light mode: #a1dcd8 on white = 1.52:1 (fail).
   For interactive text: use text-primary or text-ink. */

That leaves the dark mode axe rule re-enable as a clean, unambiguous signal: once the contrast failures are gone, the rule comes back on.


Priority order:

  1. text-accenttext-primary on all text/button/link elements (Critical, admin is actively broken)
  2. text-ink/60text-ink-2 across the board (High, affects most pages)
  3. Add the protective comment to layout.css (Low, prevents regression)
**Design review — Leonie Voss, UX/Brand** I've read the code behind both failures. They're two distinct problems with different root causes and different fix strategies. --- ## Finding 1 — `text-ink/60` on canvas (3.97:1) · **HIGH** **Where:** `persons/+page.svelte:37` (subtitle), `persons/+page.svelte:43` ("New person" link), plus `conversations/+page.svelte:53`, `ConversationTimeline.svelte:60`, `DocumentList.svelte:31` — the same pattern is scattered across the app. **Root cause:** `text-ink/60` is an opacity shortcut. It produces a computed colour that depends entirely on the background beneath it. On white (`#ffffff`) it gives `#61788e` at 4.8:1 — borderline pass. On our sandy canvas (`#f0efe9`) the same opacity blends to `#61788e`-equivalent at 3.97:1 — fail. The token system exists precisely to prevent this kind of background-dependency, but then opacity modifiers silently bypass it. **WCAG criterion violated:** 1.4.3 (Contrast, AA) — 4.5:1 required for normal text, 3:1 for large text (≥18px regular or ≥14px bold). **Fix — two cases:** *Subtitles / descriptive secondary text* (`<p class="… text-sm text-ink/60">`): → Replace with `text-ink-2`. `#4b5563` is 6.6:1 on canvas — WCAG AA ✓. Secondary text should be readable; we're using opacity to make it look subtle when the right tool is a dedicated token at the correct tone. *Subtle action links* (`<a class="… text-sm font-medium text-ink/60 … hover:text-ink">`): → Replace with `text-ink-3`. `#6b7280` is 4.8:1 on white. Note: `layout.css` comments "use only on surface, not canvas" for `ink-3` — that note needs revisiting. On canvas `#f0efe9` the effective ratio is ~4.6:1, which technically still passes AA for 14px/medium weight. However, interactive elements (links) should ideally exceed AA. I would use `text-ink-2` here too, and achieve the "subtle" look through `font-weight: 500` and `uppercase tracking-widest` rather than a low-contrast colour. **Rule going forward:** Ban `text-ink/60`, `text-ink/70`, and all opacity-modified ink utilities on canvas backgrounds. The semantic token layer is supposed to eliminate these guesses. --- ## Finding 2 — `text-accent` on white (1.52:1) · **CRITICAL** **Where:** `admin/UsersTab.svelte:81`, `admin/GroupsTab.svelte:138`. Also present in `CommentThread.svelte:280` (Reply button), `DocumentList.svelte:171` (Clear search button), `PdfViewer.svelte:300` (Direkt öffnen link) — the axe run on `/admin` surfaced the worst instances but the underlying problem is a systemic misuse of the accent token. **Root cause:** `--c-accent` in light mode is `#a1dcd8` — a pale mint chosen as a *decorative* accent (borders, icon tints, hover fills). Its luminance is too high for readable text on white. A 1.52:1 ratio is not a borderline failure; it renders these labels nearly invisible to anyone with reduced contrast sensitivity, and completely invisible in certain low-vision conditions. An admin-only screen is no justification — administrative users include senior researchers managing their family's archive. **WCAG criteria violated:** 1.4.3 (Contrast, AA — normal text). Also implicates 2.4.6 (Headings and Labels) because these are the only textual labels identifying the edit action on each table row. **Fix:** Replace `text-accent` with `text-primary` (`--c-primary` = `#012851`, 16.8:1 on white) everywhere it is used on text or interactive labels. Mint is not a text colour — it belongs on borders (`border-accent`), icon containers, and hover/focus rings. The `text-accent` utility should probably not exist at all, or be intentionally restricted to decorative/non-text contexts via a comment in the token file. ```diff - class="text-sm font-bold tracking-wide text-accent uppercase hover:text-ink" + class="text-sm font-bold tracking-wide text-primary uppercase hover:text-ink-2" ``` Dark mode note: `--c-accent` remaps to `#00c7b1` (turquoise) in dark mode — significantly better contrast on dark surfaces. That's why axe passes in dark mode. The fix (`text-primary`) also remaps correctly in dark mode to `#a1dcd8`, which is 8.4:1 on dark surface — still fine. --- ## Broader audit note These two axe findings are the tip of a pattern. A quick grep shows `text-accent` used as a text colour in at least 6 files, and `text-ink/60` in at least 5. I'd recommend the fix PR also adds a comment block to `layout.css` above `--c-accent`: ```css /* ⚠ accent — decorative use only (borders, icon tints, backgrounds) Do NOT use as text colour in light mode: #a1dcd8 on white = 1.52:1 (fail). For interactive text: use text-primary or text-ink. */ ``` That leaves the dark mode axe rule re-enable as a clean, unambiguous signal: once the contrast failures are gone, the rule comes back on. --- **Priority order:** 1. `text-accent` → `text-primary` on all text/button/link elements (Critical, admin is actively broken) 2. `text-ink/60` → `text-ink-2` across the board (High, affects most pages) 3. Add the protective comment to `layout.css` (Low, prevents regression)
Sign in to join this conversation.
No Label bug ui
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#147