xeve has been dark-only since day one. The #0f0e12 background, the #151517 cards, the orange accent — all hardcoded across 155 files and 1,800+ color references. Today, that changes. xeve now has a light mode, and it looks nothing like a generic white dashboard.
The Palette: Warm Linen, Not Clinical White
Most light modes are sterile. White backgrounds, gray borders, blue accents — they look like enterprise software. We wanted something that felt like a well-designed instrument sitting on a desk: warm, tactile, industrial.
The light palette uses #f5f4f0 (warm linen) for page backgrounds and #eeede8 (warm cream) for card surfaces. Text is #1a1917 — a warm near-black, not pure #000. Borders are #d0cfc8, a warm stone gray. Every value has a yellow-brown undertone that matches the industrial aesthetic.
The orange (#ff4f00) stays exactly the same in both modes. It is the brand constant — the one color that never changes.
Architecture: CSS Custom Properties + Tailwind v4
The key insight that made this feasible without rewriting every component: Tailwind CSS v4's @theme directive can reference CSS custom properties. We defined 13 semantic color tokens — page, surface, border, text-primary through text-muted, accent — as CSS variables that swap via [data-theme="light"].
Registering them in @theme means Tailwind generates utilities automatically: bg-page, text-text-primary, border-border. The bulk of the migration was find-and-replace: bg-[#151517] becomes bg-surface, text-[#e5e5e5] becomes text-text-primary.
For Recharts charts that use JavaScript color props, we built a useThemeColors() hook that reads computed CSS variables and returns them as a plain object. Chart components destructure the colors they need and pass them as fill/stroke/style props.
Anti-FOUC: No Flash on Load
The classic problem with client-side theme switching: the page renders in the default theme before JavaScript runs, causing a flash. We solve this with a blocking inline script that runs before any rendering:
The script reads localStorage (or falls back to prefers-color-scheme) and sets data-theme on the document element synchronously. By the time React hydrates, the correct theme is already active.
155 Files, Zero New Dependencies
The entire light mode implementation touches 155 files but adds only two new components: ThemeProvider (30 lines) and ThemeToggle (20 lines). No next-themes package. No CSS-in-JS library. Just CSS custom properties and a React context.
The toggle lives in the dashboard sidebar footer (next to your avatar) and in the landing page nav. Theme persists across sessions via localStorage.
Category Colors Stay Put
Data visualization colors — the greens, reds, blues, and yellows that represent categories in charts — are intentionally unchanged. Mid-saturation colors work on both dark and light backgrounds. Only the structural colors (backgrounds, borders, text) participate in the theme switch.