---
title: "Light Mode — A Warm Industrial Palette That Actually Feels Right"
description: "xeve now has a light mode. Not clinical white — warm linen backgrounds, bold contrast, and the same orange accent. Built with CSS custom properties and zero new dependencies."
date: "2026-03-21"
app: orma
category: "building-xeve"
readingTime: "5 min read"
---

<p>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.</p>

<h2>The Palette: Warm Linen, Not Clinical White</h2>

<p>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.</p>

<p>The light palette uses <strong>#f5f4f0</strong> (warm linen) for page backgrounds and <strong>#eeede8</strong> (warm cream) for card surfaces. Text is <strong>#1a1917</strong> — a warm near-black, not pure #000. Borders are <strong>#d0cfc8</strong>, a warm stone gray. Every value has a yellow-brown undertone that matches the industrial aesthetic.</p>

<p>The orange (#ff4f00) stays exactly the same in both modes. It is the brand constant — the one color that never changes.</p>

<h2>Architecture: CSS Custom Properties + Tailwind v4</h2>

<p>The key insight that made this feasible without rewriting every component: <strong>Tailwind CSS v4's @theme directive can reference CSS custom properties</strong>. We defined 13 semantic color tokens — page, surface, border, text-primary through text-muted, accent — as CSS variables that swap via <code>[data-theme="light"]</code>.</p>

<p>Registering them in @theme means Tailwind generates utilities automatically: <code>bg-page</code>, <code>text-text-primary</code>, <code>border-border</code>. The bulk of the migration was find-and-replace: <code>bg-[#151517]</code> becomes <code>bg-surface</code>, <code>text-[#e5e5e5]</code> becomes <code>text-text-primary</code>.</p>

<p>For Recharts charts that use JavaScript color props, we built a <code>useThemeColors()</code> 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.</p>

<h2>Anti-FOUC: No Flash on Load</h2>

<p>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:</p>

<p>The script reads <code>localStorage</code> (or falls back to <code>prefers-color-scheme</code>) and sets <code>data-theme</code> on the document element synchronously. By the time React hydrates, the correct theme is already active.</p>

<h2>155 Files, Zero New Dependencies</h2>

<p>The entire light mode implementation touches 155 files but adds only two new components: ThemeProvider (30 lines) and ThemeToggle (20 lines). No <code>next-themes</code> package. No CSS-in-JS library. Just CSS custom properties and a React context.</p>

<p>The toggle lives in the dashboard sidebar footer (next to your avatar) and in the landing page nav. Theme persists across sessions via localStorage.</p>

<h2>Category Colors Stay Put</h2>

<p>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.</p>
