Theme System
This guide documents how dark/light theme switching is implemented in Waldur HomePort.
Architecture Overview
The theme is one attribute: data-theme="light|dark" on <html>. Everything follows it:
- Design tokens. Every semantic token file has a
:root[data-theme='dark']block, sovar(--bg-primary)and friends switch. - Tailwind. The
dark:variant keys off the same attribute. - Bootstrap / Metronic.
src/metronic/sass/style.scssis one stylesheet for both themes: its colours read the tokens, and the few pairings no token has use thedark-thememixin (core/base/mixins/_theme.scss), which emits:where([data-theme='dark']) &— the same specificity as the rule it overrides:
1 2 3 4 5 6 7 | |
There used to be a second, dark compile (style.dark.scss, $mode: dark, with the SCSS ramps
inverted by isDarkMode()), swapped in when the theme changed. It is gone; don't reintroduce a
theme-specific SCSS value. A colour inside an SVG data URI, which a var() cannot reach, is a
function of its colour (_svg-icons.scss) with a dark override in _dark-icons.scss;
css-color(gray-dark, 500) gives the CSS ramp value as Sass.
Key Files
| File | Purpose |
|---|---|
src/theme/ThemeProvider.tsx |
React context provider for theme state |
src/theme/useTheme.ts |
Hook to access current theme |
src/theme/ThemeStorage.ts |
localStorage persistence |
src/theme/utils.ts |
Injects the Metronic stylesheet once, sets data-theme, initial theme detection |
src/theme/ThemeSwitcher.tsx |
UI toggle component |
src/metronic/sass/_tokens.scss |
Ring and focus-ring variables |
src/metronic/sass/_colors.scss |
Derived color variables ($success, $text-muted, brand ramp, …) |
src/metronic/sass/_color-ramps.scss |
Theme-aware color ramps — generated, edit packages/design-tokens/tokens/colors.json (see design-tokens.md) |
src/metronic/sass/core/base/mixins/_theme.scss |
dark-theme / light-theme mixins, mirrored-ramp-color() |
Theme Initialization Flow
Application.tsxwraps the app withThemeProviderThemeProvidercallsgetInitialTheme()which checks:- localStorage for saved preference
- OS
prefers-color-schemesetting - Falls back to 'light'
loadTheme()injects the Metronic stylesheet (once) and setsdata-themeon<html>, which switches the CSS custom properties, Tailwind'sdark:variant and the Metronicdark-themerules- User toggles update localStorage and
data-theme; nothing is reloaded
Usage Patterns
Pattern 1: Semantic Tailwind Utilities (Recommended)
Use the semantic utilities. Each one is exactly the design token of the same name, so it
follows the theme without a dark: twin:
1 2 3 4 5 6 7 8 | |
Theme-aware utilities (full reference in design-tokens.md):
| Utility | Token | Light → Dark |
|---|---|---|
bg-primary |
--bg-primary |
White → near-black |
bg-secondary |
--bg-secondary |
gray-50 → dark gray |
bg-tertiary / bg-quaternary |
--bg-tertiary / --bg-quaternary |
gray-100 / 200 → darker grays |
text-primary … text-senary |
--text-primary … --text-senary |
gray-900 … 300 → light grays |
border-primary |
--border-primary |
Control border |
border-secondary |
--border-secondary |
Card border |
text-brand |
--brand-color |
Brand (tenant colour) |
For the brand colour use text-brand (and border-brand, outline-brand). State colours (success, warning, danger) use the
status matrix: bg-warning-subtle text-warning.
Bootstrap emits no colour utilities: its old class names (text-muted, text-success,
bg-light, …) paint nothing, and every colour class is Tailwind's (text-muted →
text-quaternary). Never pair Bootstrap's border class with a Tailwind border colour: the
colour never paints (use border-[1px]). See
design-tokens.md § Bootstrap colour utilities.
Pattern 2: useTheme() Hook
For third-party libraries that need explicit theme values:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Pattern 3: CSS Custom Properties in SCSS & CSS
When writing custom SCSS or CSS, use semantic CSS custom properties directly:
1 2 3 4 5 | |
Common semantic tokens (defined in packages/design-tokens/src/css/semantic/; the full
list with values is in design-tokens.md § Semantic roles):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | |
Pattern 4: CSS Custom Properties
For runtime theming needs, use CSS custom properties:
1 2 3 4 5 6 7 8 9 | |
Common Pitfalls
Static Brand Colour in SCSS
$primary and $primary-N compile to a static default green; the tenant's brand is the runtime
--brand-* ramp. Paint brand in SCSS with var(--bg-brand-solid), var(--fg-brand-primary),
var(--brand-600) and so on, never $primary. See
design-tokens.md § The brand colour in SCSS.
Hardcoded Colors
Avoid hardcoding colors that won't adapt:
1 2 3 4 5 | |
Third-Party Components
Components like react-select, Monaco Editor, and ECharts need explicit theme configuration via the useTheme() hook. Check existing implementations for examples:
src/form/select/theme.ts- react-select themingsrc/form/MonacoField.tsx- Monaco Editor themingsrc/core/EChart.tsx- ECharts theming
Testing Dark Mode
- Toggle theme using the switcher in the user dropdown menu
-
Check that:
-
Text is readable against backgrounds
- Borders are visible but not harsh
- Interactive elements have visible hover states
- Third-party components match the overall theme