Source:
ocean/docs/adr/ADR-045-theme-system-implementation.md| ✏️ Edit on GitHub
ADR-045: Theme System Implementation
Status
Accepted
Context
Ocean needed a sophisticated theme system that supports:
- Multiple color palettes (neutral, stone, zinc, gray, slate)
- Automatic light/dark mode variants
- Type-safe theme definitions
- Consistent design tokens across the application
- Easy theme switching without page reload
The system needed to integrate with Tailwind CSS while maintaining performance and developer experience.
Decision
We implemented a CSS custom properties-based theme system with the following architecture:
1. Design Token Architecture
-
Source of Truth: JSON files in
/design-tokens/palettes.json: Color theme definitionsbase-tokens.json: Non-color tokens (spacing, typography, shadows)
-
Build Process: Style Dictionary generates CSS from JSON tokens
- Outputs to
src/styles/*.generated.css - Automatic generation on dev/build via npm scripts
- Outputs to
2. Dual-Attribute Theme Control
-
Color Theme:
data-color-themeattribute on<html>- Controls which color palette is active
- CSS rules target
[data-color-theme="theme-name"]
-
Light/Dark Mode:
classattribute on<html>- Managed by
next-themesfor system preference support - CSS rules use
.darkclass for dark variants
- Managed by
3. CSS Variable Strategy
All theme values are exposed as CSS custom properties:
:root[data-color-theme='neutral'] {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
/* ... more variables */
}
.dark[data-color-theme='neutral'] {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
/* ... dark mode overrides */
}
4. React Integration
-
Providers: Two context providers manage theme state
ThemeProvider(next-themes): Light/dark modeColorThemeProvider: Color palette selection
-
Hooks:
useTheme(): Access light/dark mode controlsuseColorTheme(): Access color palette controls
Implementation Details
File Structure
/design-tokens/
├── palettes.json # Color theme definitions
└── base-tokens.json # Non-color tokens
/src/
├── styles/
│ ├── color-themes.generated.css # Generated color themes
│ ├── tokens.generated.css # Generated base tokens
│ └── tokens.css # Import wrapper
├── config/
│ └── themes.ts # TypeScript theme types
└── hooks/
└── use-color-theme.tsx # Theme management hook
Build Scripts
{
"scripts": {
"tokens:build": "style-dictionary build",
"tokens:themes": "node tools/generate-themes-ts.js",
"tokens": "pnpm run tokens:build && pnpm run tokens:themes",
"predev": "pnpm run tokens",
"prebuild": "pnpm run tokens"
}
}
Generated File Sizes
color-themes.generated.css: 272 lines (all color theme variants)tokens.generated.css: 394 lines (spacing, typography, etc.)
Consequences
Positive
- Performance: CSS custom properties enable instant theme switching without re-render
- Type Safety: Generated TypeScript definitions ensure compile-time safety
- Maintainability: Single source of truth in JSON makes updates easy
- Flexibility: Supports unlimited color themes with automatic dark variants
- Integration: Works seamlessly with Tailwind CSS utilities
- Developer Experience: Themes auto-generate on dev/build
Negative
- Build Dependency: Requires build step to generate CSS from tokens
- File Size: Each theme adds ~50 lines of CSS (minimal impact)
- Complexity: Two-provider system may be confusing initially
Neutral
- Browser Support: Requires browsers that support CSS custom properties (all modern browsers)
- Learning Curve: Developers need to understand the dual-attribute system
Example Usage
// Component using theme
export function Card({ children }) {
return <div className="bg-background text-foreground border rounded-lg">{children}</div>
}
// Theme switcher
export function ThemeSwitcher() {
const { colorTheme, setColorTheme } = useColorTheme()
const { theme, setTheme } = useTheme()
return (
<>
<select value={colorTheme} onChange={(e) => setColorTheme(e.target.value)}>
<option value="neutral">Neutral</option>
<option value="slate">Slate</option>
{/* ... more options */}
</select>
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Toggle dark mode
</button>
</>
)
}
References
- Style Dictionary Documentation
- next-themes Documentation
- CSS Custom Properties MDN
- Related ADRs:
- ADR-010: Tailwind CSS v4 with Vite Integration
- ADR-032: OKLch Color Space for Theme System