Issue description
The issue exactly as filed - the problem this plan exists to solve.- Issue ID
- ENGG-981
- Issue title
- Build dark mode for the web-app
- Issue description
- Customers have requested that the app theming match the system UI and cited eye strain.
- Related issues
- DESIGN-229 | WCAG Contrast ratios failing on secondary CTAs
ENGG-884 | Tokens for color variables not assigned
Assumptions
Judgment calls made to fill gaps the issue itself left unspecified.3A repo-wide audit found roughly 340 raw hex/rgba values outside the token file; each needs conversion before theming can work.
[A]Ship dark mode only for already-tokenized components first, and migrate the rest incrementally post-launch.
[B]Run an automated migration script that converts and tokenizes all 340 values up front, before writing any theme logic.
Once a user manually picks a theme, that choice persists across sessions regardless of later OS-level changes.
[A]Always follow the OS setting live, with no manual override at all.
[B]Add a settings-page toggle so users explicitly opt in to syncing with OS-level theme changes.
The support widget and analytics banner don’t expose a dark variant; theming them ourselves risks visual drift on every vendor update.
[A]Wrap embeds in a dimming overlay so they don’t look broken against a dark page.
[B]Hide third-party embeds entirely in dark mode until each vendor ships a themeable variant.
Coding Plan Phases
The implementation, broken into phases and tasks - phases can be built independently of each other, but a phase's own tasks depend on one another in order.3Every hardcoded color becomes a semantic token with light/dark values, so the rest of the plan can switch themes by remapping one layer instead of touching component code.
- Audit src/**/*.css and *.tsx for hex/rgba literals and group them into semantic roles: --bg, --text, --border, --accent.
- Flag ambiguous cases (e.g. a color used for both a border and a status color) for design review.
- Add tokens.css with a light :root block and a [data-theme="dark"] override block.
- Point the Tailwind config at the token layer so utility classes resolve through it instead of raw values.
Reads the OS-level preference on first load, then remembers whatever the user explicitly picks afterward - so the default is smart but never sticky against their own choice.
- Use a matchMedia('(prefers-color-scheme: dark)') listener so a live OS change updates the theme immediately, until a manual override exists.
- Render a theme-neutral shell on the server to avoid a light-to-dark flash on first paint.
- Store the choice in localStorage under theme-preference, checked before the OS listener on every load.
- Set a data-theme attribute on <html> that the token layer keys off of.
Every component gets checked against both themes for contrast and legibility, since a token swap alone doesn’t guarantee the same visual hierarchy still reads correctly in the dark palette.
- Script axe-core against every route in both themes and fail CI on any WCAG AA regression.
- Track known exceptions in an allowlist so real regressions aren’t lost in the noise.