<html> element flips the CSS-variable palette, so switching is instant with no rebuild and no page reload.
Prerequisites
Theming needs nothing connected. The toggle works on the very first screen, before you connect a runtime or deploy a team, because the preference lives in the browser, not on the server.
- A browser with
localStorage(every modern browser). The preference is stored per origin, so two ports, for example a dev server on:5173and the bundled server on its API port, keep independent theme settings.
Steps
1. Find the theme toggle
Open the dashboard. The toggle (ThemeToggle) sits at the bottom of the left sidebar (AgentListColumn), below a divider, under the navigation buttons. It renders a Lucide icon, the current preference label, and, when you are on System, the resolved theme in small uppercase to its right.
| Current preference | Icon | Label | Right-side hint |
|---|---|---|---|
| Light | Sun | Light | , |
| Dark | Moon | Dark | , |
| System | Monitor | System | light or dark (whatever the OS resolves to) |
2. Click to cycle
The toggle is a single button that cycles through the three preferences in a fixed order on each click:aria-label) always names what the next click will do: for example “Theme: Light. Click for Dark.”, or on System “Theme: System (dark). Click for Light.”. There is no separate menu; one button, one cycle.
3. (Optional) Let it follow your OS
Pick System to track your operating-system color scheme. While in system mode, Clawboo subscribes to theprefers-color-scheme: dark media query and flips the resolved theme live when you change your OS appearance, no click required. Picking Light or Dark explicitly unsubscribes from that media query and pins the theme.
How it resolves and persists
Two concepts are kept distinct:- Preference (
theme): the raw choice you made:system,light, ordark. - Resolved theme (
resolvedTheme): the concrete palette currently rendered:lightordark, neversystem. In system mode it is derived from the OS media query; otherwise it equals the preference.
localStorage under the key clawboo.theme. On the next load, ThemeProvider reads that key:
- A stored value of
light,dark, orsystemis honored. - Any other state, including a fresh install with no stored value, defaults to light, so onboarding always happens in light mode.
dark adds the .dark class to document.documentElement (<html>); light removes it. Everything visual follows from that class.
No flash on load (FOUC prevention)
If theming only ran inside React, a dark-mode user would briefly see the light default before the bundle mounted and applied the class. To prevent that flash, an inline<script> in index.html runs synchronously before React mounts. It reads the same clawboo.theme key, resolves the preference (consulting prefers-color-scheme only for system), and sets or removes the .dark class on <html> immediately. ThemeProvider then re-applies the identical result on mount.
The inline init script in
index.html is a hand-mirrored copy of the resolver in ThemeProvider.tsx: same key, same default-to-light fallback, same prefers-color-scheme check. They must stay in sync. If you change one resolver, change the other, or a hard refresh in dark mode will flash light.How the palette switches
There are no two stylesheets and no JavaScript re-styling.globals.css declares one set of Tailwind tokens that resolve to CSS variables, then defines the variable values twice:
:rootholds the light values (--background: #f8fafc,--foreground: #0f172a,--primary: #dc2a48, …) pluscolor-scheme: light..darkoverrides the same variables with the dark production values (--background: #0a0e1a,--foreground: #e8e8e8, …) pluscolor-scheme: dark.
@theme inline block maps semantic color tokens (--color-background, --color-foreground, --color-primary, and so on) onto those variables, and a @custom-variant dark (&:where(.dark, .dark *)) makes the .dark class the dark-mode switch. So toggling the single .dark class on <html> re-resolves every token at once, instant, no rebuild. Light-mode brand colors are deepened one step (for example primary red #dc2a48 vs. the dark #e94560) for AA contrast on white.
Options / variations
| Preference | Behavior | OS-tracking |
|---|---|---|
light | Always light palette | No |
dark | Always dark palette | No |
system | Follows the OS color scheme, live | Yes, re-resolves on OS change |
The welcome / onboarding splash screen is intentionally theme-independent; it always renders a bright day-sky backdrop with dark-pinned text regardless of your theme choice. Your light/dark/system selection still applies everywhere else in the dashboard.
Verify it worked
- Click the toggle and watch the whole UI repaint in one frame, no reload, no flash mid-app.
- Inspect
<html>in the browser devtools: thedarkclass is present in dark mode (and when System resolves to dark) and absent in light mode. - Check
localStoragefor theclawboo.themekey; it should hold your last explicit choice (light,dark, orsystem). - Hard-refresh while in dark mode: the page should load dark immediately, with no light flash, proving the inline init ran before React.
Troubleshooting
My theme reset to light. The preference is stored per browser origin. Clearing site data, switching browsers, or opening the dashboard on a different port (for example dev
:5173 vs. the bundled server) starts from the light default until you choose again.Related
- Design system, the token catalog, surface tiers, and motion behind the palette
- Dashboard tour, where the sidebar and views live
- Using Clawboo, the full feature map