feat: user-selectable color themes (theme registry)
Problem
Right now the color palette is hardcoded in src/index.css under a single :root block (light) and .dark block (dark). If you want a different color scheme, the only option today is to fork and maintain a full CSS diff, then re-merge it by hand on every upstream release.
I've been running a Gruvbox-based fork for a few months exactly this way: a full override of every CSS variable in index.css, rebased on every update. Not sustainable for a personal fork, and not something the project should have to carry either.
Evidence there's demand
- #1079 (open) asks for the theme to follow OS appearance - easier to reason about once themes are externalized from
index.cssin the first place. - #907 and #787 (closed) were bugs about the code editor / dark mode not syncing - symptoms of theme state living in more than one place.
Proposed design
Goal: don't break anything currently running, don't change the default look.
a. Extract color variables out of index.css
Move the existing :root / .dark block into a default theme file, e.g. src/themes/default.css, scoped under [data-theme="default"] and [data-theme="default"].dark. Add src/themes/gruvbox.css with the same variable set. No component changes needed - everything already consumes hsl(var(--background)) etc.
b. Extend ThemeContext.jsx
Currently it only exposes { isDarkMode, toggleDarkMode }. Add:
{ isDarkMode, toggleDarkMode, theme, setTheme, availableThemes }setTheme writes document.documentElement.dataset.theme and persists to localStorage (color-theme). isDarkMode stays as-is - light/dark remains its own axis, independent from the color palette.
c. Theme registry
src/themes/index.ts exporting [{ id, name, previewColors }]. Adding a theme becomes: one CSS file + one entry in this array.
d. Settings UI A dropdown in Settings with a small color swatch preview per option.
e. Sync meta[name="theme-color"]
ThemeContext.jsx currently hardcodes the iOS status bar color per mode:
themeColorMeta.setAttribute('content', '#141414'); // dark
themeColorMeta.setAttribute('content', '#f6f4ef'); // lightReading the active --background value instead is a small fix that falls naturally out of this work.
f. Scope of the first PR
Just default + one more theme, to keep the diff reviewable. Additional themes would be follow-up PRs.
Screenshot
Attaching below - my daily driver running the Gruvbox palette purely via CSS variable overrides, no component code touched. That's roughly the shape of what a second theme would look like.
Question for maintainers
If this direction looks reasonable, I'm happy to build it. One thing I'd like your take on before starting: should theming live entirely in core (as above), or would you rather core just expose the registry/hook and themes ship as plugins? Core-first seems like the safer incremental step either way, since plugins currently only have a tab slot (server/utils/plugin-loader.js) with no way to touch global styles - but wanted to check your preference before writing code.
Source: siteboon/claudecodeui