v4.29 — Primary Foreground Contrast Remediation
Every
--primary/--primary-foregroundand--accent/--accent-foregroundpair 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-foregroundand--accent/--accent-foregroundpair 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
masternever had.ionic_frontend/tests/unit/themeContrast.spec.tsstatically parses every theme block and asserts all 40--primary/--accentforeground pairs (10 themes × 2 modes × primary/accent) clear 4.5:1, plus a dedicated--primary-interactivedeclaration-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.tsdrives all 20 theme × mode permutations with axe'scolor-contrastrule 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
.vuefiles 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 standarddisabled:opacity-50convention; 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/--backgroundrather than--primaryitself, 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
masterfirst. 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
| Phase | Name | Status | Plans | Highlights |
|---|---|---|---|---|
| 1 | Token decision & preset remediation | Shipped | 3 | All 40 token pairs matched the design spec; guard demonstrated 31/17-failing → 40/0-failing (PR #2004) |
| 2 | Consumer audit & remediation | Shipped | 5 | --primary-interactive + --primary-accessible shipped; 115-file consumer audit; 3 real fixes; gradient family carved out (PRs #2019, #2026) |
| 3 | Runtime verification | Shipped | 1 | 20-permutation real-browser axe sweep, hover wiring proven, guard mutation-tested (PR #2033) |
| 4 | Token reconciliation onto master | Shipped | 1 | Reconciled onto v4.26's already-shipped values; promoted the milestone to master in the same merge (PR #2035) |
| 5 | Prevention & docs | Shipped | 1 | Design-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 reachedmasterfirst, with a strictly more complete token set — four families v4.29's branch lacked, plus dark-mode--primary-interactivev4.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.tspassed cleanly againstmaster'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-interactiveand--primary-accessibleship 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-foregroundand--accent/--accent-foregroundpair 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.
masternow 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
masteralso revertedGoal.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 bytokenDrift.spec.tsand 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.
Related Artifacts
- Roadmap: v4.29-primary-foreground-contrast-remediation-ROADMAP.md
- Milestone Audit: v4.29-primary-foreground-contrast-remediation-MILESTONE-AUDIT.md
- Source issue: OBJ-2055
- Deferred follow-up: OBJ-2075 (26 gradient cells, unscheduled); OBJ-2087 (regression-test coverage for the four consumer-layer fixes)
- Regression fix: OBJ-2086 — #2041
- Merge PR: #2035 — token reconciliation + master promotion
Related Commits
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 ontomaster's v4.26 values — also the milestone's master promotion (PR #2035)93d855461— Phase 5: prevention & docs (PR #2040)e6a4671ef— OBJ-2086: re-applyGoal.vue:79cover-photo badge contrast fix (PR #2041)
Last updated: 2026-08-03