Skip to content

v4.29 — Primary Foreground Contrast Remediation

Every --primary/--primary-foreground and --accent/--accent-foreground pair clears WCAG AA 4.5:1 in both modes across all ten themes, and a static + runtime guard now stops a future theme from shipping failing.

Summary

The app's default UiButton — the primary call-to-action on every screen — failed WCAG AA contrast in light mode on 7 of 10 supporter themes. Ocean, a paid Supporter theme, measured 2.69:1 against a 4.5:1 floor. The accent token family failed worse: 9 of 10 themes, in both light and dark mode. The root cause was mechanical and uniform across the whole theme file — every light-mode theme block hardcoded a flat-white foreground, while every dark-mode block correctly derived a same-hue near-black ink instead — and nothing in the codebase guarded the asymmetry.

v4.29 fixed both token families, added a previously-undefined --primary-interactive hover token (which had been silently falling through to the Default theme's blue on every Supporter theme), added --primary-accessible for a related backlog item, audited and remediated every non-flat consumer pairing across the app, and shipped the accessibility guard master never had: a static token-parsing test and a real-browser axe sweep across all 20 theme × mode permutations, so a new theme or a future edit can't silently regress this again.

Partway through, the milestone discovered that a separately-numbered milestone — v4.26 Light-Mode Contrast Floor — had independently shipped a fix for the same defect on the same files, reaching master first. Rather than doubling the change, v4.29 reconciled its token layer onto v4.26's already-shipped values and shipped on the parts only it delivered: the guard suite, the consumer audit and its four fixes, and the two new token families. The color values users see for Default, Rose, and Ember today shipped with v4.26, not this milestone — v4.29's own contribution is the guard that keeps them correct, plus the consumer-level fixes and new tokens layered on top.

Goal

Make every --primary/--primary-foreground and --accent/--accent-foreground pair clear WCAG AA 4.5:1 in both modes across all ten themes — and leave behind a static contrast guard so a future theme cannot ship failing.

Scope — What Shipped

  • The contrast guard master never had. ionic_frontend/tests/unit/themeContrast.spec.ts statically parses every theme block and asserts all 40 --primary/--accent foreground pairs (10 themes × 2 modes × primary/accent) clear 4.5:1, plus a dedicated --primary-interactive declaration-and-contrast check. Demonstrated failing on the pre-fix tree (31 declared, 17 failing) before being trusted.
  • A real-browser verification tier. ionic_frontend/tests/e2e/specs/accessibility/theme-contrast-sweep.cy.ts drives all 20 theme × mode permutations with axe's color-contrast rule confirmed actually running (not silently skipped), including a resolved-hover-fill assertion that proves the hover class reaches the right token at runtime — the one thing the static guard can't see.
  • The full consumer audit. 115 .vue files and 194 occurrences, plus 45 raw-CSS usages, classified against their real backdrops. Four fixes shipped: the disabled "Adopting…" button on the goal-quiz card moved to the repo's standard disabled:opacity-50 convention; a decorative streak-refresh icon moved off primary-foreground ink onto background ink; a camera-hover chip over a user photo moved to an opaque scrim; and a cover-photo "Add photo" badge over the placeholder image got a legible dark scrim.
  • Two new token families. --primary-interactive — previously undefined in every Supporter light theme, so hovering a themed primary button silently turned it Default blue — is now declared per theme. --primary-accessible, derived against --card/--background rather than --primary itself, ships for a related backlog item (OBJ-2056) with no consumer inside this milestone.
  • What did not ship here: the primary-color values for Default, Rose, and Ember — those shipped with v4.26, which reached master first. A 26-cell gradient family (--gradient-primary/--primary-light/--primary-glow) measured as unfixable by any available ink swap; tracked as a follow-up, OBJ-2075.

Phases

PhaseNameStatusPlansHighlights
1Token decision & preset remediationShipped3All 40 token pairs matched the design spec; guard demonstrated 31/17-failing → 40/0-failing (PR #2004)
2Consumer audit & remediationShipped5--primary-interactive + --primary-accessible shipped; 115-file consumer audit; 3 real fixes; gradient family carved out (PRs #2019, #2026)
3Runtime verificationShipped120-permutation real-browser axe sweep, hover wiring proven, guard mutation-tested (PR #2033)
4Token reconciliation onto masterShipped1Reconciled onto v4.26's already-shipped values; promoted the milestone to master in the same merge (PR #2035)
5Prevention & docsShipped1Design-tokens page, README, CHANGELOG, planning delta (PR #2040)

Key Decisions

  • Reconcile onto master's v4.26 values rather than ship v4.29's own. Discovered mid-milestone that v4.26 (a differently-numbered milestone, unrelated in name) had independently fixed the same defect and reached master first, with a strictly more complete token set — four families v4.29's branch lacked, plus dark-mode --primary-interactive v4.29 had only declared for light mode. Promoting v4.29's tokens as-is would have regressed those and reintroduced the exact dark-mode hover fallthrough bug Phase 2 existed to fix. Reconciling cost nothing accuracy-wise: themeContrast.spec.ts passed cleanly against master's values with no changes.
  • Insert a fourth phase (token reconciliation) rather than force the guard/audit to carry mismatched values. Discovered 2026-08-02, after Phase 3 shipped; the docs phase moved from Phase 4 to Phase 5 to make room.
  • The gradient family (--gradient-primary/--primary-light/--primary-glow) is a documented, deliberate carve-out, not a silent gap. The consumer audit proved by computation — both available inks tried against all 26 failing cells — that no ink-only fix exists; the real fix requires a token redesign this milestone's own scope explicitly excluded in advance. Tracked as OBJ-2075.
  • --primary-interactive and --primary-accessible ship even though the former has a narrow use (hover state) and the latter has zero consumers inside this milestone — both close known failure modes (silent fallthrough to the wrong theme's color) that would otherwise recur the next time a theme or a dependent feature is built.

Requirements Coverage

No standalone REQUIREMENTS.md — this milestone is driven by a measured accessibility defect (source issue OBJ-2055), not a formal PRD. All 11 FG-prefixed tasks in the ROADMAP's phase checklist shipped. Full detail: MILESTONE-AUDIT.md.

Outcomes

  • Every --primary/--primary-foreground and --accent/--accent-foreground pair clears 4.5:1 in both modes across all 10 themes, verified both statically and in a real browser.
  • Hovering a themed primary button now stays in that theme's own hue in every mode — the silent fallthrough to the Default theme's blue is gone.
  • The disabled "Adopting…" state, the decorative streak-refresh icon, the photo-upload hover chip, and the cover-photo badge all read clearly against their real backdrops.
  • master now has a contrast guard it never had before: a new theme, or an edit to an existing one, that regresses below 4.5:1 fails CI immediately instead of shipping silently.

Tech Debt

  • (Phase 4) A merge silently reverted a shipped consumer-layer fix, and it took a human to catch it. The reconciliation merge that promoted this milestone to master also reverted Goal.vue:79's cover-photo badge fix back to its pre-fix, failing classes — the merge's own survival check tested file presence, not line content, so it read clean. Caught after the fact and re-applied as OBJ-2086. The token-layer fixes this same merge touched were all protected by tokenDrift.spec.ts and survived; this one consumer-class change had no test of its own. Full detail in the MILESTONE-AUDIT.md.
  • (Cross-cutting) CHANGELOG.md's v4.29 entry is stale as of this page's publication — it still describes the Goal.vue regression above as unresolved, written before OBJ-2086 fixed it. Needs a short follow-up correction, not done as part of this close-out.
  • (Scope, deliberate) OBJ-2075 — 26 gradient cells (--gradient-primary/--primary-light/--primary-glow) fail AA with no available ink fix; open and unscheduled.
  • (Process, tracked) OBJ-2087 — the four consumer-layer fixes above have no regression test protecting their classes, unlike the token layer, which is why the Goal.vue reversion above went uncaught until a human found it. Filed as its own backlog item, deliberately unparented so it doesn't distort this milestone's rollup.
  • 47ed8e042 — Phase 1: token decision & preset remediation (PR #2004)
  • f27534544 — Phase 2a: --primary-interactive + --primary-accessible + Storybook story (PR #2019)
  • 2c3146d58 — Phase 2b: consumer audit remediations (PR #2026)
  • 6ceab9237 — Phase 3: runtime contrast E2E sweep (PR #2033)
  • 0806e2284 — Phase 4: token reconciliation onto master's v4.26 values — also the milestone's master promotion (PR #2035)
  • 93d855461 — Phase 5: prevention & docs (PR #2040)
  • e6a4671ef — OBJ-2086: re-apply Goal.vue:79 cover-photo badge contrast fix (PR #2041)

Last updated: 2026-08-03

Loading…