Theming
Player 1 Inventory supports three theme states: light, dark, and system (auto-detects OS preference). Both light and dark modes are fully implemented — every token has a value for both.
How it works
Token switching
All color tokens are defined in two blocks inside apps/web/src/design-tokens/theme.css:
/* Light mode — applied by default */:root { --background-base: oklch(90% 4% 85); --foreground-default: oklch(30% 8% 85); /* … */}
/* Dark mode — overrides when .dark is on <html> */.dark,[data-theme='dark'] { --background-base: oklch(10% 2% 85); --foreground-default: oklch(90% 6% 85); /* … */}When the .dark class is present on <html>, every token resolves to its dark-mode value. The [data-theme='dark'] selector covers the Starlight design guide site. No component-level theming is needed — switching the class on <html> updates the entire UI simultaneously.
Anti-flash script
Themes are applied by an inline <script> in apps/web/index.html that runs before React loads and before the browser paints. This prevents the flash of wrong theme that would occur if theme initialization happened inside a React effect.
The script reads localStorage.getItem('theme-preference'), checks matchMedia('(prefers-color-scheme: dark)') for the system state, then adds or removes the dark class on <html> immediately. It also writes window.__THEME_INIT__ so the React hook can read the already-applied state without recalculating.
localStorage key
The user’s preference is stored under the key theme-preference in localStorage. Valid values are 'light', 'dark', and 'system'.
The useTheme hook
Import useTheme from @/hooks/useTheme to read or change the theme in any component:
import { useTheme } from '@/hooks/useTheme'
function ThemeToggle() { const { preference, theme, setPreference } = useTheme()
// preference: 'light' | 'dark' | 'system' // The user's stored choice. Use this to show the active state in a toggle UI.
// theme: 'light' | 'dark' // The theme actually applied to the page. Use this for conditional rendering // (e.g. swapping a logo variant).
// setPreference: (pref: 'light' | 'dark' | 'system') => void // Updates localStorage and immediately applies the new theme.
return ( <button onClick={() => setPreference(preference === 'dark' ? 'light' : 'dark')}> {theme === 'dark' ? 'Switch to light' : 'Switch to dark'} </button> )}The hook also listens for OS-level theme changes via matchMedia. When preference is 'system', toggling the OS dark mode setting updates theme in real time without requiring a page reload.
Three-state preference
| Preference value | Behavior |
|---|---|
'light' | Always light, regardless of OS |
'dark' | Always dark, regardless of OS |
'system' | Follows OS preference; updates in real time |
The default preference is 'system'.
Rules for adding new tokens
When adding a new CSS custom property to the token system:
- Always define both light and dark values. Add the property to both the
:rootblock and the.dark, [data-theme='dark']block intheme.css. - Map to a Tailwind utility in the
@theme inlineblock so components can usebg-,text-, orborder-classes. - Use OKLCH format. Adjust the L channel for contrast — target L=35–55% in light mode and L=75–85% in dark mode for text tokens; L=90–98% light / L=10–30% dark for background tokens.
- Test in both modes before merging.
/* Example: adding a new token */
:root { --my-new-token: oklch(60% 20% 195); /* light value */}
.dark,[data-theme='dark'] { --my-new-token: oklch(75% 20% 195); /* dark value */}
@theme inline { --color-my-new-token: var(--my-new-token); /* → bg-my-new-token, text-my-new-token */}Do not use dark: prefix
Because all tokens already switch via CSS custom properties, the dark: Tailwind prefix is rarely needed. Reaching for dark: is a signal that you may be bypassing the token system.
// Wrong — hardcoded values that bypass tokens<div className="bg-white dark:bg-gray-900" />
// Correct — semantic token that adapts automatically<div className="bg-background-surface" />The only legitimate use of dark: is for one-off non-token properties that genuinely differ between modes and don’t warrant a new token (e.g. a specific icon opacity tweak).