Dark-mode contrast pass + themed monochrome emoji & UI polish - #31
Open
ZubairQazi wants to merge 16 commits into
Open
ZubairQazi wants to merge 16 commits into
ZubairQazi wants to merge 16 commits into
Conversation
The accent palette and the muted/faint text ramps were below WCAG AA in
*both* themes, not just dark. Measured across 8 views: 678 sub-AA text
elements in dark, 569 in light, with near-identical counts for the shared
values (148x alpha 0.5, 95x alpha 0.45, 49x #1D7FC4 ...).
- --text-muted 0.45 -> 0.70 light / 0.66 dark, --text-faint 0.22 -> 0.60 /
0.55. The old values measured 2.9:1 and 1.7:1; the small uppercase labels
on --text-faint were the least readable text in the app.
- Accents gain -rgb channel twins and paired --tint-* washes, with
per-theme values. The required correction differs per hue (purple and red
needed +33% white in dark, green and amber +5-6%), so one blanket lighten
would have washed out two of them for nothing.
- Dark tints are the accent hue sunk into --bg. The light pastels were being
reused unchanged in dark mode, which is why the two Discover banner cards
rendered cream-on-near-white at 1.01:1 -- fully invisible.
- designTokens.ts referenced its own alphas (0.50/0.25/0.10/0.18) that had
drifted from index.css (0.45/0.22/0.08/0.12), so one token rendered two
different colors depending on whether a component used CSS or an inline
style. It now points at the vars.
Call-site conversion follows; `${MC}12`-style hex-alpha concatenation breaks
against a var() and is handled next.
Transparency was applied by concatenating a hex alpha suffix -- `${color}18`
or `color + '18'`. That only works while `color` is a 6-digit hex. Now that
the accents are var() references it yields `var(--c-jun)18`, which is not a
valid color, so the browser drops the whole declaration and the border or
wash silently disappears. Nothing throws and nothing logs, which is what made
this worth centralising rather than fixing case by case.
withAlpha() resolves a themed token through its -rgb channel twin and still
handles the raw hex neutrals that remain (e.g. APP_STATUS_META's #7A6D5C), so
call sites don't need to know which kind of color they were handed.
166 hardcoded accent and tint literals across 23 files become var(--c-*) /
var(--tint-*), so they follow the theme instead of rendering light-mode
colors under [data-theme="dark"].
The pastel tints mattered most: CollegeDiscoverTab's two banner cards passed
#EDEAF7 / #EBF5F0 as a `tint` prop, which stayed near-white in dark mode
while the text went cream -- both cards measured 1.01:1 and were effectively
invisible.
Also fixes the concatenation sites this breaks (`${color}18` against a var()
yields an invalid color the browser drops), and two pre-existing instances of
the same bug in contentBlocks.tsx and ChecklistContentView.tsx, where
SUCCESS_GREEN and MODULE_COLORS were already var() references.
data/timelineData.ts's accentDim held the same palette pre-expanded as decimal
channels, so it had already drifted from its `accent` twin; it now shares the
-rgb vars.
DeadlineEvent gains a colorRgb companion, since consumers (Dashboard, Calendar,
Timeline) need the accent at partial opacity and can no longer get there by
string concatenation.
Kept separate because it is the single biggest offender -- 71 hex literals,
12 eight-digit hex-alpha literals and 43 concatenation sites, more than any
other file -- and it is not otherwise touched by this branch, so a reviewer
can read the other 23 files without it in the way.
The MC pattern (`${MC}40`, `MC + '50'`) appears 43 times here. MC is
MODULE_COLORS.financialAid, now var(--c-fresh), so every one of those was
already emitting an invalid color and dropping its declaration. They go
through withAlpha().
The status/urgency metadata maps pair a `color` with a `bg`; both sides moved
together (accent -> var(--c-x), its pastel -> var(--tint-x)) so the pairings
survive. One-off colors with no mapped token (#B26A00, #8A8F98, #1F7A54,
#FFF3E0) are left as literals.
The migration only holds if a new hardcoded hex fails CI. Otherwise the next component written in light mode reintroduces the bug and nobody notices until someone opens dark mode -- which is exactly how this branch's worst case happened, three files written after dark mode landed that all hardcoded light-mode literals anyway. Covers plain literals and template-literal text, since the colors are written both ways. Verified with a probe file that all three forms error and that var(--c-jun) does not, then confirmed `eslint src` is clean -- so a green lint is now evidence the conversion is complete rather than an assumption. Does not cover `color + '18'` concatenation: the literal '18' is not distinguishable from ordinary string math, and a false positive in CI is worse than the grep. withAlpha() is the documented alternative.
Re-running the audit after the conversion surfaced four things the first pass missed. Dark 110 -> 20 remaining, light 142 -> 20. - Accents were derived against the bare surfaces, but chips draw them on rgba(accent, 0.15) over --elevated, which is lighter. That left them at 4.47:1 -- passing on paper, failing where they are actually drawn. Re-derived against the washed backdrop too. - --on-accent: text sitting ON an accent has to invert with the theme, because the accents do. Hardcoded white measures 6.6:1 in light and 2.4:1 in dark; ink is the reverse. Applied to the 7 rules that put white on an accent fill. - --glass-rgb: frosted surfaces were literal rgba(255,255,255,a) and stayed white in dark mode. The "you are here" badge was cream text on near-white at 1.01:1. The white-on-accent highlights (.tour-intro-btn, .st-toggle-knob) are deliberately left as literal white -- they sit on a colored fill. - index.css still held 18 palette usages the ESLint rule cannot see, since it only lints ts/tsx. Those were the light-mode "Open ->" links at 3.0:1. Also fixes .auth-sso-btn--apple, which set color:#fff on background:var(--text) -- cream on cream in dark mode. Neither audit could catch it: both start after login, so the auth screen is never measured. The 20 remaining flags in each theme are all .dash-module-emoji. Those are false positives: color emoji paint from the emoji font and ignore the CSS `color` property, so comparing that color to the background measures nothing.
# Conflicts: # web/src/components/CalendarPage.tsx # web/src/components/EssaysModule.tsx # web/src/components/TimelinePage.tsx # web/src/data/applicationDeadlines.ts
Self-host Noto Emoji and alias it onto every text family with an emoji-only unicode-range, so emoji resolve to the monochrome face even where a component sets font-family inline. A mono() helper strips U+FE0F and appends U+FE0E to force text presentation. Every emoji render site is wrapped in mono(); glyphs now inherit the themed text colour and flip with light/dark instead of showing the glossy system colour emoji.
Add --c-*-fill tokens (vivid in light, lighter pastels in dark) and point the module-card banners at them. The accent tokens stay AA-dark for text; the fills are only used behind the white on-accent glyphs, so the banners read lively again instead of the muddy dark tone the contrast pass left.
The Caveat subtitle rendered near-illegible at 14px in muted grey (low x-height). Bump to 18px/600 in a stronger ink — keeps the handwriting motif while making the tagline readable.
The colour logo's dark quill vanished against the dark background. Add a <Logo> component + useResolvedTheme() hook that swaps to logo-white.png when data-theme is dark, and route every logo <img> through it.
The year glyph (Freshman leaf, etc.) rendered as a neutral monochrome mark. Colour it with the year's themed accent so it flips per theme.
This branch converted MODULE_COLORS from hex to var() references, which
turned the leftover `${MC}40`/`${MC}08`/`${MC}12` hex-alpha concatenations
into `var(--c-sen)40` — invalid CSS the browser silently drops, so the
feedback button's border/background and the AI badge wash vanished. Route
them through withAlpha().
The In-progress/Submitted/Decisions bar filled with the AA-dark accents, reading muddy in light mode. Add --c-fresh-fill and give the bar segments and legend dots the vivid fill tokens; the count numbers keep the AA-dark accent since they are text.
Add fillOf(), mapping a themed accent to its vivid --c-*-fill twin, and use it on every non-text fill: the shared Bar (so all module progress bars brighten at once), the Financial Aid gauge + dots, the Application Tracking stat-tile stripe, and the Knowledge Library hero. Buttons, colored text and count numbers keep the AA-dark accent for contrast.
Deploying timeline-prototype with
|
| Latest commit: |
f4d29d8
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://299f7c16.timeline-prototype.pages.dev |
| Branch Preview URL: | https://fix-dark-mode-contrast.timeline-prototype.pages.dev |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the app render correctly in both light and dark themes, plus a round of UI polish. Builds on the earlier AA-contrast palette work and adds a consistent, theme-aware treatment for emoji, banners, logos, and accent fills.
Changes
Contrast / theming foundation (earlier commits)
var()tokens; lint rule rejecting hardcoded palette colors.This round of polish
font-family; amono()helper forces text presentation. Glyphs inherit the themed color and flip with light/dark.--c-*-filltokens (vivid in light, pastel in dark) so banners aren't muddy, while accent text stays AA-dark.<Logo>+useResolvedTheme()swap to the white logo whendata-themeis dark.${MC}xxhex-alpha concatenation became invalid onceMODULE_COLORSwerevar(); routed throughwithAlpha().fillOf()maps an accent to its vivid fill twin, applied to progress bars, the Aid Engine gauge/dots, submission-pipeline bar, stat-tile stripes, and hero boxes. Buttons/text/count-numbers stay AA-dark (white-text contrast).Accessibility note
Non-text surfaces (bars, dots, gauges, icons ≥3:1) use vivid fills; accent-colored text and white-on-accent buttons keep the darker AA value (≥4.5:1). Verified with measured contrast.
Verification
tsc✓,eslint✓,212/212tests ✓.