candle-annotator/openspec/changes/archive/2026-02-13-add-dark-light-mode/design.md
2026-02-13 09:37:18 +01:00

4.5 KiB

Context

The app is a Next.js 16 candlestick chart annotation tool using Tailwind CSS 3 with shadcn-ui style CSS variables (HSL values in :root). Tailwind is already configured with darkMode: ["class"], meaning the .dark class on <html> controls dark mode. Currently only light-mode CSS variables are defined in globals.css, and the hacker-theme spec describes a dark-only neon green aesthetic. The app needs to support both themes with system detection and manual override.

Goals / Non-Goals

Goals:

  • Detect system color scheme preference (prefers-color-scheme) and apply matching theme on first load
  • Provide a manual toggle in the UI to switch between dark, light, and system modes
  • Persist the user's choice in localStorage
  • Avoid flash of wrong theme (FOUC) on page load
  • Define dark-mode CSS variables that maintain the hacker-theme aesthetic
  • Define light-mode CSS variables that provide a clean, readable alternative while keeping monospace typography and terminal feel

Non-Goals:

  • Custom user-defined color themes beyond dark/light
  • Per-component theme overrides
  • Server-side theme detection (SSR will use system default, hydration applies preference)

Decisions

1. Use next-themes for theme management

Choice: Install and use next-themes library.

Rationale: next-themes is the standard solution for Next.js theme switching. It handles:

  • System preference detection via prefers-color-scheme media query
  • Adding/removing the .dark class on <html> (matches our existing darkMode: ["class"] Tailwind config)
  • localStorage persistence
  • FOUC prevention via inline script injection
  • SSR compatibility

Alternatives considered:

  • Custom React context + useEffect: Would work but requires reimplementing FOUC prevention, system detection, and localStorage sync. next-themes is battle-tested and small (~2KB).
  • CSS-only with prefers-color-scheme: No manual toggle capability, which is a requirement.

2. Three-mode toggle: System / Light / Dark

Choice: Offer three modes — System (follows OS), Light (forced), Dark (forced).

Rationale: Users who want to match their OS get automatic switching. Users who prefer a specific theme can lock it in. This is the standard UX pattern.

3. CSS variable approach for both themes

Choice: Define :root (light) and .dark (dark) CSS variable sets in globals.css.

Rationale: The existing setup already uses HSL CSS variables consumed by Tailwind. Adding a .dark selector block with dark-mode values requires zero changes to component code — all bg-background, text-foreground, etc. classes automatically switch. The hacker-theme neon green palette maps to the dark variant. The light variant uses neutral/slate tones with green accents.

4. Theme toggle placement

Choice: Place the theme toggle button in the sidebar, near the top or bottom of the tool list.

Rationale: The sidebar is always visible and is the existing control surface. A small icon button (sun/moon/monitor icons from lucide-react) keeps it unobtrusive. Three-state cycle: click rotates through system → light → dark.

5. ThemeProvider wraps the app in layout.tsx

Choice: Wrap {children} with <ThemeProvider> from next-themes in the root layout.

Rationale: This is the standard integration point. The provider must be a client component, so we'll create a thin ThemeProvider wrapper component.

Risks / Trade-offs

  • FOUC on first load: next-themes injects a blocking script to prevent this, but in rare edge cases (slow JS) users might see a brief flash. → Mitigation: next-themes handles this well out of the box; attribute="class" config ensures Tailwind picks it up immediately.

  • Chart library theming: lightweight-charts has its own color configuration that's set programmatically, not via CSS. → Mitigation: The chart component will need to read the current theme and pass appropriate colors to the chart instance. Listen to theme changes and update chart colors accordingly.

  • Hacker theme divergence: The light theme won't have neon glow effects or scanlines — these only make sense on dark backgrounds. → Mitigation: This is intentional. The light theme keeps monospace fonts and terminal structure but uses conventional colors for readability.

  • Component styles relying on hardcoded colors: If any components use hardcoded hex colors instead of CSS variables, they won't switch. → Mitigation: Audit components during implementation and replace hardcoded values with CSS variable references.