This document outlines the strategy for implementing dark mode in the Credence Frontend.
| Component | Status | Required Changes |
|---|---|---|
| Global Shell | index.css uses hardcoded hexes for background and text. |
|
| Layout | Header uses background: #fff and borderBottom: 1px solid #e2e8f0 in inline styles. |
|
| Banners | Pastel backgrounds (#eff6ff, #f0fdf4) are too bright for dark mode. |
|
| Toasts | Shares same color logic as Banners; needs dark-optimized surface tokens. | |
| Badges | ✅ Ready | Can be easily updated by switching internal tokens. |
| States | Empty states and Loading skeletons use hardcoded grays. | |
| Pages | Bond.tsx and TrustScore.tsx use inline styles for card backgrounds and borders. |
We will use CSS variables defined in :root and overridden in [data-theme='dark'].
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--bg-page |
#f8fafc (Slate 50) |
#020617 (Slate 950) |
Main page background |
--bg-card |
#ffffff |
#0f172a (Slate 900) |
Card/Header background |
--text-primary |
#0f172a |
#f8fafc |
Primary headings and text |
--text-secondary |
#64748b |
#94a3b8 |
Supporting text |
--border-default |
#e2e8f0 |
#1e293b |
Default borders |
| Token | Light Value | Dark Value | Usage |
|---|---|---|---|
--color-primary |
#0284c7 |
#38bdf8 |
Buttons, links, primary accents |
--color-focus |
rgba(2, 132, 199, 0.5) |
rgba(56, 189, 248, 0.5) |
Focus rings |
For Banners and Toasts in Dark Mode, we will use tinted dark backgrounds instead of pastels.
| Severity | Light BG | Dark BG (Tinted Slate 900) |
|---|---|---|
| Info | #eff6ff |
rgba(59, 130, 246, 0.1) |
| Success | #f0fdf4 |
rgba(34, 197, 94, 0.1) |
| Warning | #fffbeb |
rgba(245, 158, 11, 0.1) |
| Danger | #fef2f2 |
rgba(239, 68, 68, 0.1) |
- Phase 1: Define tokens in
src/index.css. - Phase 2: Refactor
Layout,Bond, andTrustScoreto use CSS variables instead of hardcoded hexes in inline styles. - Phase 3: Update
Banner.css,Toast.css, andBadge.cssto use status tokens. - Phase 4: Add a
ThemeTogglecomponent toLayout.tsx.
The theme has exactly one owner: SettingsContext (src/context/SettingsContext.tsx).
- State + persistence:
themeMode('light' | 'dark' | 'system') lives inSettingsContextand is persisted under the singlecredence:settingslocalStorage key. There is no separate'theme'key. - Document attribute:
SettingsContextis the only writer ofdocument.documentElement[data-theme]. Its effect resolves'system'viamatchMedia('(prefers-color-scheme: dark)')and re-applies on OS changes. ThemeToggle(src/components/ThemeToggle.tsx) is a pure consumer ofuseSettings(). It owns no theme state and writes to no storage key. It:- derives the displayed light/dark from
themeMode(resolving'system'viamatchMedia), - subscribes to
matchMediaso its icon,aria-pressed, andaria-labelstay in sync withdata-themewhen the OS theme changes insystemmode, - on click calls
setThemeModewith the explicit opposite of the currently resolved theme (never back to'system').
- derives the displayed light/dark from
This removes the historical desync where the toggle kept its own state under a
duplicate 'theme' key while the document was driven by credence:settings.
Older builds of ThemeToggle persisted the theme under a standalone 'theme'
localStorage key. On first load, SettingsContext performs a one-time
migration so returning users keep their preference:
- If
credence:settingsalready carries athemeMode, it wins (the single source of truth) and the legacy value is discarded. - Otherwise, a valid legacy
'theme'value ('light' | 'dark' | 'system') seedsthemeMode; invalid values are ignored and fall back to'system'. - The migrated value is folded into
credence:settings, and the orphan'theme'key is removed on mount.
The migration is transparent: it does not register as an unsaved change on the Settings page.