Theming
Single source of truth theming with complete color inversion between light and dark modes.
Quick Start
Call getTheme() once in your root layout, then pass it down via props to child components.
This provides explicit data flow and better testability.
Child Component Pattern
Why pass as props instead of calling getTheme() everywhere?
- Explicit data flow - You see what state each component depends on
- Better testing - Can inject mock state easily
- Single initialization - Clear where state originates
- Efficiency - Singleton runs once, not on every navigation
SvelteKit Execution Order
Understanding when code runs helps you choose the right pattern for state initialization. This is based on analysis of SvelteKit's internal source code.
+layout.server.js ✓ - Server-only data fetching+layout.js ✓ ✓ Runs on every navigation+layout.svelte script ✓ ✓ Runs once on component mountonMount() - ✓ Client-only, after hydrationKey Insight from SvelteKit Source
+layout.js runs on every navigation, not just initial load.
For singletons like theme state, this means wasted work on each page change.
+layout.svelte script runs once when the layout mounts.
This is more efficient for global state that doesn't change between navigations.
When to Use Which Pattern
+layout.svelte Recommended for Theme
Use for singletons (theme, toast, global UI state)
- Runs once per mount
onMountseparates SSR from client code- Module singleton ensures single instance
- Pass via props for explicit data flow
+layout.js
Use for data that needs server context or async fetching
- Can use
awaitandfetch() - Has access to
parent()data - Integrates with SvelteKit's invalidation
- Data available via
$page.data
ThemeState API
Properties (Reactive)
current 'light' | 'dark' | 'system' User's theme preferenceresolved 'light' | 'dark' Actual theme after resolving 'system'dataTheme 'miozu-light' | 'miozu-dark' Value for data-theme attributecurrentTheme 'miozu-light' | 'miozu-dark' Alias for dataTheme (backwards compat)isDark boolean True if resolved theme is darkisLight boolean True if resolved theme is lightMethods
init() void Load from storage, setup system preference listenersync() void Sync state with current DOM attribute (for hydration)set(theme) void Set theme preference ('light', 'dark', or 'system')toggle() void Toggle between light and dark (skips system)cleanup() void Remove media query listener (rarely needed)Static Methods
ThemeState.getThemeFromCookieString(cookies) 'miozu-light' | 'miozu-dark' Parse theme from cookie header (for SSR)Storage
miozu-theme'light' | 'dark' | 'system''miozu-light' | 'miozu-dark'SSR & Flash Prevention
Add this inline script to your app.html before any stylesheets:
Server-Side Rendering
Use the static method to parse theme from cookies in your +layout.server.js:
Migration Guide
⚠️ Breaking Changes in v0.4.0
If you were using the old context-based API or custom storage keys, follow this migration guide.
From Context API
From Custom Storage Keys
If your app used custom storage keys (e.g., admin-theme, theme),
update your app.html to migrate:
Property Name Changes
currentTheme dataTheme currentTheme still works (alias)isInitialized Removed Use init() in onMount insteadinitialize() init() Renamed for brevitysetTheme() set() Renamed for brevityTechnical Philosophy
🎯 Base16 Foundation
Miozu uses a carefully calibrated Base16 palette where each color has a specific semantic meaning. Colors 00-07 form a luminosity gradient from darkest to lightest, while 08-0F provide accent colors with consistent perceptual weight.
⚖️ Mathematical Inversion
Dark mode isn't just "dark colors" — it's a complete mathematical inversion of the luminosity scale. Base00 (background) swaps with Base07, maintaining the same relative contrast relationships.
🚀 Single Source of Truth
Call getTheme() once in your root layout, then pass it down via props.
Explicit data flow, easy testing, clear ownership of state.
⚡ CSS-Only Switching
Theme changes happen via CSS attribute selectors — no JavaScript re-renders. Instant theme switching with zero layout shift.