@open-kingdom/shared-frontend-ui-theme
v0.0.2-16
Published
Centralized theming system providing a `Theme` object structure, CSS custom property generation applied to `document.documentElement`, a React context/provider for runtime light/dark mode switching with localStorage persistence, and a base Tailwind CSS co
Readme
@open-kingdom/shared-frontend-ui-theme
Centralized theming system providing a Theme object structure, CSS custom property generation applied to document.documentElement, a React context/provider for runtime light/dark mode switching with localStorage persistence, and a base Tailwind CSS configuration that maps all design tokens to the generated CSS variables.
Exports
Provider and Hook
| Export | Type | Description |
| --------------- | ------------------------------ | --------------------------------------------------------------------------------------- |
| ThemeProvider | React.FC<ThemeProviderProps> | Context provider; applies theme CSS variables to DOM on mount and on theme/mode changes |
| useTheme | () => ThemeContextValue | Returns current theme state and setters; throws if used outside ThemeProvider |
Default Themes
| Export | Type | Description |
| ------------------- | ------- | ------------------------------------------------------------------------------ |
| defaultLightTheme | Theme | Full default light theme object (blues primary, slate secondary, zinc neutral) |
| defaultDarkTheme | Theme | Dark variant — same as light but with inverted neutral palette |
Utilities
| Export | Signature | Description |
| ----------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| generateCSSVariables(theme) | (theme: Theme) => Record<string, string> | Converts Theme object to flat { '--color-primary-500': '#3b82f6', ... } map |
| applyThemeToDOM(theme) | (theme: Theme) => void | Calls generateCSSVariables and sets all vars on document.documentElement.style; no-op in SSR |
| mergeThemes(base, override) | (base: Theme, override: Partial<Theme>) => Theme | Deep-merges two themes, preserving unspecified color shades from base |
| saveThemeMode(mode) | (mode: ThemeMode) => void | Saves mode to localStorage under key 'theme-mode' |
| loadThemeMode() | () => ThemeMode \| null | Reads mode from localStorage; returns null if not set or invalid |
Types
| Export | Type | Description |
| ------------------- | ------------------- | --------------------------- |
| Theme | interface | Full theme object structure |
| ThemeMode | 'light' \| 'dark' | Mode discriminant |
| ThemeContextValue | interface | Return type of useTheme() |
| ThemeColors | interface | Color section of Theme |
| ColorPalette | interface | Single color scale (50–950) |
| TypographyScale | interface | Font size scale |
| SpacingScale | interface | Spacing scale |
| BorderRadiusScale | interface | Border radius scale |
| ShadowScale | interface | Box shadow scale |
| FontFamily | interface | Font family arrays |
Type Definitions
ThemeProviderProps
| Property | Type | Required | Default | Description |
| -------------- | ----------------- | -------- | ------------------- | -------------------------------------- |
| children | React.ReactNode | Yes | — | Content to wrap with the theme context |
| initialTheme | Theme | No | defaultLightTheme | Starting theme object |
| initialMode | ThemeMode | No | 'light' | Starting light/dark mode |
ThemeContextValue
| Property | Type | Description |
| ---------- | --------------------------------- | ------------------------------------------------------------------------------- |
| theme | Theme | The currently active theme object |
| mode | ThemeMode | The current mode ('light' or 'dark') |
| setMode | (mode: ThemeMode) => void | Switches mode and persists the choice to localStorage |
| setTheme | (theme: Partial<Theme>) => void | Deep-merges the provided partial theme with the current theme via mergeThemes |
ColorPalette
Each color scale has optional shade keys from 50 through 950 (all string values representing CSS color values). The shades are 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, and 950.
ThemeColors
| Property | Type | Description |
| ----------- | -------------- | ------------------------------------------------ |
| primary | ColorPalette | Primary brand color scale |
| secondary | ColorPalette | Secondary color scale |
| neutral | ColorPalette | Neutral/grey color scale (inverted in dark mode) |
| success | ColorPalette | Success state color scale |
| warning | ColorPalette | Warning state color scale |
| error | ColorPalette | Error state color scale |
Theme
| Property | Type | Description |
| ----------------------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
| colors | ThemeColors | Color palettes for all semantic scales |
| typography.fontFamily | FontFamily | Font family arrays for sans, serif, and mono |
| typography.fontSize | TypographyScale | Font size entries from xs through 6xl; each value is a size string or [size, { lineHeight }] tuple |
| spacing | SpacingScale | Spacing values for xs, sm, md, lg, xl, 2xl, 3xl |
| borderRadius | BorderRadiusScale | Border radius values for none, sm, md, lg, xl, full |
| boxShadow | ShadowScale | Box shadow values for sm, md, lg, xl, 2xl |
CSS Variables Generated
generateCSSVariables(theme) produces CSS custom properties set on :root (document.documentElement). All variables follow these naming patterns:
Colors: --color-{scale}-{shade} for each shade (50–950) of each color scale (primary, secondary, neutral, success, warning, error).
Typography: --font-family-sans, --font-family-serif, --font-family-mono; --text-{size} for font sizes; --leading-{size} for line heights (both from xs through 6xl).
Spacing: --spacing-{key} for each key (xs, sm, md, lg, xl, 2xl, 3xl).
Border radius: --radius-{key} for each key (none, sm, md, lg, xl, full).
Box shadow: --shadow-{key} for each key (sm, md, lg, xl, 2xl).
Default Theme Values
defaultLightTheme (colors excerpt)
| Token | 500 value |
| ----------- | ----------------------- |
| primary | #3b82f6 (blue-500) |
| secondary | #64748b (slate-500) |
| neutral | #737373 (neutral-500) |
| success | #22c55e (green-500) |
| warning | #f59e0b (amber-500) |
| error | #ef4444 (red-500) |
Font families: sans: ['Inter', 'system-ui', 'sans-serif'], serif: ['Georgia', 'serif'], mono: ['JetBrains Mono', 'ui-monospace', 'monospace']
defaultDarkTheme
Identical to defaultLightTheme except the neutral palette is inverted (50 ↔ 950 etc.) so text/background colors flip for dark mode.
Setup
Basic setup
import { ThemeProvider } from '@open-kingdom/shared-frontend-ui-theme';
export function App() {
return (
<ThemeProvider>
{/* All CSS variables are now set on :root */}
<YourApp />
</ThemeProvider>
);
}With custom initial theme
import { ThemeProvider, defaultLightTheme } from '@open-kingdom/shared-frontend-ui-theme';
import type { Theme } from '@open-kingdom/shared-frontend-ui-theme';
const brandTheme: Theme = {
...defaultLightTheme,
colors: {
...defaultLightTheme.colors,
primary: {
...defaultLightTheme.colors?.primary,
500: '#7c3aed', // override mid-range to purple
600: '#6d28d9',
},
},
};
<ThemeProvider initialTheme={brandTheme} initialMode="light">
<App />
</ThemeProvider>;Usage in Components
useTheme hook
useTheme() returns a ThemeContextValue with the current theme, the current mode ('light' or 'dark'), a setMode function that switches modes and persists the choice to localStorage, and a setTheme function that deep-merges a partial theme override with the current theme.
import { useTheme } from '@open-kingdom/shared-frontend-ui-theme';
function ThemeToggle() {
const { mode, setMode } = useTheme();
return <button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>Switch to {mode === 'light' ? 'dark' : 'light'} mode</button>;
}Runtime theme override
import { useTheme } from '@open-kingdom/shared-frontend-ui-theme';
function BrandSwitcher() {
const { setTheme } = useTheme();
const applyBrand = () => {
setTheme({
colors: {
primary: { 500: '#7c3aed', 600: '#6d28d9' },
},
});
// CSS variables update immediately on document.documentElement
};
}Using CSS variables directly
.my-component {
background-color: var(--color-primary-500);
color: var(--color-neutral-900);
font-family: var(--font-family-sans);
font-size: var(--text-lg);
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
}Using Tailwind utility classes
All Tailwind classes referencing theme tokens use CSS variables as their values, so they automatically reflect runtime theme changes:
<div className="bg-primary-500 text-neutral-50 rounded-md shadow-md p-md">
<h1 className="text-2xl font-sans">Hello</h1>
</div>Tailwind Configuration
Using the base config as a preset in your app
// tailwind.config.js (in your application or library)
const baseConfig = require('@open-kingdom/shared-frontend-ui-theme/tailwind.config.js');
module.exports = {
presets: [baseConfig],
content: [
'./src/**/*.{ts,tsx}',
// Include paths of UI libraries you consume:
'../../libs/shared/frontend/ui-notifications/src/**/*.{ts,tsx}',
'../../libs/shared/frontend/feature-notifications/src/**/*.{ts,tsx}',
],
theme: {
extend: {
// Your app-specific overrides:
},
},
};What the base config does
The base config extends Tailwind's default theme with CSS-variable-backed values for all design tokens. This means Tailwind generates utility classes like bg-primary-500 whose actual color value resolves at runtime from var(--color-primary-500). Changing the theme via ThemeProvider / setTheme updates the CSS variables and all Tailwind-class-styled elements update instantly without a rebuild.
Utility Function Examples
import { generateCSSVariables, applyThemeToDOM, mergeThemes, saveThemeMode, loadThemeMode, defaultLightTheme, defaultDarkTheme } from '@open-kingdom/shared-frontend-ui-theme';
// Generate variable map (useful for SSR/injection):
const vars = generateCSSVariables(defaultLightTheme);
// { '--color-primary-50': '#eff6ff', '--color-primary-500': '#3b82f6', ... }
// Apply to DOM directly (ThemeProvider does this automatically):
applyThemeToDOM(defaultDarkTheme);
// Deep-merge two themes:
const merged = mergeThemes(defaultLightTheme, {
colors: { primary: { 500: '#7c3aed' } },
});
// Persist and restore mode:
saveThemeMode('dark');
const savedMode = loadThemeMode(); // 'dark' | 'light' | nullArchitecture
The package is structured as follows:
src/index.ts— Public exports: theme types, defaults, provider, and utilitiestailwind.config.js— Base Tailwind preset (CSS-variable-backed tokens); exported directly from the package root via theexportsmap, resolving todist/tailwind.config.jssrc/lib/theme.types.ts—Theme,ThemeMode,ThemeContextValue,ColorPalette, and related interfacessrc/lib/default-theme.ts—defaultLightThemeanddefaultDarkThemeobjectssrc/lib/theme-provider.tsx—ThemeProviderReact component anduseThemehooksrc/lib/theme-utils.ts—generateCSSVariables,applyThemeToDOM,mergeThemes,saveThemeMode,loadThemeMode
Theme flow
ThemeProviderinitializes withdefaultLightThemeand checkslocalStoragefor a saved mode- On mount and on every
themechange,applyThemeToDOM(theme)sets CSS variables on:root - When mode changes,
ThemeProviderswitches betweendefaultLightThemeanddefaultDarkTheme(unless a custominitialThemewas passed) - All Tailwind classes and direct
var()CSS references update instantly saveThemeModepersists the selected mode so it survives page reloads
Best Practices
- Use semantic color tokens (
primary-500,error-700) rather than hardcoded hex values - Always include dark mode variants for components that use theme colors:
text-neutral-900 dark:text-neutral-100 - Pass the
Themeobject fromuseTheme()directly toDataGrid'sthemeprop — it accepts theUIThemesubset - Call
setThemefor brand overrides rather than replacing the entire theme — it deep-merges with the current values - Do not use Tailwind
dark:variants based onprefers-color-scheme; use themodestate fromuseTheme()and apply the dark theme object throughThemeProviderinstead
Testing
nx test shared-frontend-ui-theme