Frontend: dark mode is half-built dead code — wire a theme toggle or remove it #59

Closed
opened 2026-06-06 18:53:06 +00:00 by logaritmisk · 1 comment
Owner

Severity: Medium. From a frontend UX audit. Decide-and-commit.

Problem

web/src/index.css:53-72 defines a complete .dark token set and the ui/* components carry dark: variants (button.tsx, input.tsx, badge.tsx, checkbox.tsx). But (a) nothing ever applies the .dark class and there is no theme toggle (grep → no setTheme/classList/next-themes), so dark mode can never activate; and (b) the feature screens use light-only literals (bg-neutral-50, text-neutral-400, bg-green-100, …) that wouldn't adapt anyway. Dark mode is currently maintenance overhead that can't turn on, and if it did, half the app would be unreadable.

Suggested fix

Decide:

  • Ship it: add a theme toggle (persisted, with a system-preference default) that toggles .dark on <html>. Completing the token migration (see the design-system issue) makes most of the app adapt automatically.
  • Drop it: remove the .dark token block and the dark: variants until it's actually wanted.

Source: frontend UX/design audit, 2026-06-06.

**Severity: Medium.** _From a frontend UX audit. Decide-and-commit._ ## Problem `web/src/index.css:53-72` defines a complete `.dark` token set and the `ui/*` components carry `dark:` variants (`button.tsx`, `input.tsx`, `badge.tsx`, `checkbox.tsx`). But (a) nothing ever applies the `.dark` class and there is **no theme toggle** (grep → no `setTheme`/`classList`/`next-themes`), so dark mode can never activate; and (b) the feature screens use light-only literals (`bg-neutral-50`, `text-neutral-400`, `bg-green-100`, …) that wouldn't adapt anyway. Dark mode is currently maintenance overhead that can't turn on, and if it did, half the app would be unreadable. ## Suggested fix Decide: - **Ship it:** add a theme toggle (persisted, with a system-preference default) that toggles `.dark` on `<html>`. Completing the token migration (see the design-system issue) makes most of the app adapt automatically. - **Drop it:** remove the `.dark` token block and the `dark:` variants until it's actually wanted. _Source: frontend UX/design audit, 2026-06-06._
Author
Owner

Shipped — merged to main (9323c60). Took the "ship it" path (the #49 token migration made the screens dark-ready).

What landed:

  • Tri-state toggle (Light / Dark / System) — an icon segmented control (lucide Sun/Moon/Monitor) in the header next to the language switch, with aria-pressed + aria-label. Default is System.
  • Framework-free core (web/src/theme/theme.ts): resolveTheme/readTheme/applyTheme toggling .dark on <html>, all DOM globals guarded.
  • useTheme hook (web/src/theme/use-theme.ts): persists to localStorage, and while in System mode subscribes to prefers-color-scheme so the app live-tracks the OS until the user pins a value (listener torn down when pinned).
  • No FOUC: a synchronous inline <script> in index.html applies the resolved class before first paint; its logic matches theme.ts exactly (no double-apply flash).
  • Dark --primary contrast: nudged dark --primary/--ring to oklch(0.72 0.18 277) — near-black button label now 6.7:1 (independently recomputed), comfortably AA. (Note: the parked "3.21:1" figure was a faulty measurement; the prior value already passed at ~5.7:1. Light mode unchanged.)

Client-only (no /api/config field), no new npm dependency (lucide already present). en/sv parity; tests (theme core unit + ThemeSwitch interaction + story); gate green: typecheck, lint, 181 tests, build, check:size (184.4 KB gz), check:colors; no codename.

Follow-ups (not in scope): per-account/server-synced theme default (default_theme on ConfigView, mirroring default_language); cross-tab sync via a storage listener; a dedicated dark-mode visual QA pass across every screen.

Shipped — merged to `main` (`9323c60`). Took the "ship it" path (the #49 token migration made the screens dark-ready). **What landed:** - **Tri-state toggle** (Light / Dark / System) — an icon segmented control (lucide Sun/Moon/Monitor) in the header next to the language switch, with `aria-pressed` + `aria-label`. Default is **System**. - **Framework-free core** (`web/src/theme/theme.ts`): `resolveTheme`/`readTheme`/`applyTheme` toggling `.dark` on `<html>`, all DOM globals guarded. - **`useTheme` hook** (`web/src/theme/use-theme.ts`): persists to `localStorage`, and while in System mode subscribes to `prefers-color-scheme` so the app **live-tracks the OS** until the user pins a value (listener torn down when pinned). - **No FOUC**: a synchronous inline `<script>` in `index.html` applies the resolved class before first paint; its logic matches `theme.ts` exactly (no double-apply flash). - **Dark `--primary` contrast**: nudged dark `--primary`/`--ring` to `oklch(0.72 0.18 277)` — near-black button label now **6.7:1** (independently recomputed), comfortably AA. (Note: the parked "3.21:1" figure was a faulty measurement; the prior value already passed at ~5.7:1. Light mode unchanged.) Client-only (no `/api/config` field), **no new npm dependency** (lucide already present). en/sv parity; tests (theme core unit + ThemeSwitch interaction + story); gate green: typecheck, lint, 181 tests, build, check:size (184.4 KB gz), check:colors; no codename. Follow-ups (not in scope): per-account/server-synced theme default (`default_theme` on `ConfigView`, mirroring `default_language`); cross-tab sync via a `storage` listener; a dedicated dark-mode visual QA pass across every screen.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logaritmisk/biggus-dickus#59