Skip to content

feat: dark mode with a System / Light / Dark setting - #81

Open
jibraaan wants to merge 1 commit into
CopilotKit:mainfrom
jibraaan:pr/dark-mode
Open

jibraaan wants to merge 1 commit into
CopilotKit:mainfrom
jibraaan:pr/dark-mode

Conversation

@jibraaan

@jibraaan jibraaan commented Oct 4, 2026

Copy link
Copy Markdown

Workflow

Settings has a new Appearance control: System, Light or Dark. System follows the device and updates live. The choice is saved per browser, and the theme is applied before the first paint, so there's no white flash.

Approach: dark colors derived at build time

style.css and editor.css contain about 560 hard-coded colors, most of them unique. Hand-writing dark overrides for all of them would be large and would drift with every style change. Instead, a small PostCSS plugin (src/build/dark-theme.ts, registered in vite.config.ts) generates the dark theme:

  • For every rule that has color declarations, it emits a sibling rule under :root[data-theme='dark'] that contains only those color declarations, inside the same media query.
  • Colors are mapped in OKLab: lightness is flipped (white becomes #1b1b1b, black becomes #eeeeee), with a curve that lifts mid-tones so secondary text keeps AA contrast. Hue is preserved and chroma is softened slightly. Alpha is kept.
  • Shadows stay dark: their tint is dropped and their alpha kept, so they never turn into glows. Keyframes and url() values are left alone.
  • Cascade order is preserved: every dark rule gains the same specificity and sits right after its original. Every color-property declaration gets a twin, even background: none or var(--x). Without that, an earlier rule's dark twin could beat a later light rule that has no literal color.
  • Surfaces that are dark on purpose (the call view) are wrapped in /* theme: fixed */ … /* theme: end */.

New styles get a dark variant automatically.

public/theme.js sets data-theme before React loads. It's an external file because the CSP disallows inline scripts. src/client/theme.ts handles later changes and the system preference. postcss is now listed as a direct dev dependency; it was already installed through Vite.

Verification

  • npm run check-format, lint, typecheck, test (171 passing, 7 new in tests/dark-theme.test.ts) and build all pass.
  • Tests cover the color mapping, shadows, selector scoping, media queries, cascade-order twins, and the fixed/keyframe/url exclusions.
  • Checked in the browser on light and dark, desktop and mobile. Contrast measured in dark: secondary text 4.98:1, setup notes 4.83:1, headings 8.4:1. Light mode is unchanged.
  • Not covered: the active-chat, page-editor and call views weren't visually reviewed in dark, because they need configured services. They use the same generated rules.

🤖 Generated with Claude Code

Dark colors are generated at build time by a small PostCSS plugin
(src/build/dark-theme.ts) instead of hand-written overrides for ~560
hard-coded colors. Every rule with color declarations gets a twin under
:root[data-theme='dark'] that flips OKLab lightness while keeping hue,
lifts mid-tones for AA contrast, keeps shadows dark, and preserves media
queries and cascade order. Intentionally dark surfaces (the call view)
are marked /* theme: fixed */.

- public/theme.js applies the saved theme before first paint (an external
  file because the CSP disallows inline scripts), so there is no flash.
- Settings has an Appearance control; System follows the device and
  updates live. The choice is stored per browser.
- Measured in dark: secondary text 4.98:1, notes 4.83:1, headings 8.4:1.
  Light mode is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant