Engg-067 - Build dark mode for the web app
Ready

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.3
[1]Existing color values are hardcoded, not tokenized

A 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.

[2]prefers-color-scheme sets the default, but never overrides an explicit choice

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.

[3]Third-party embeds won’t be themed in v1

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.3
Phase 1Design token layer

Every 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.

1Extract raw colors into a token schema
  • 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.
2Wire the token file into the build
  • 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.
Phase 2Theme detection & persistence

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.

1Detect prefers-color-scheme on first load
  • 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.
2Persist an explicit user choice
  • 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.
Phase 3Component pass & contrast audit

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.

1Run an automated contrast pass
  • 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.

Research

Reference material - docs, RFCs, related tickets - the plan's reasoning draws on.
Docs
Design token audit spreadsheetConfluence · updated 3 days ago
Theming architecture RFCNotion · Platform space
Linked issues
DESIGN-229 · WCAG contrast ratios failing on secondary CTAsLinear · In Progress
ENGG-884 · Tokens for color variables not assignedLinear · Backlog
Customer calls
Design review - dark palette contrast passCall notes · Aug 12
Bug reports
BUG-771 · Chart legend unreadable in dark mode (staging)Reported by @platform-oncall
3/3 phases selected