@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' | null

Architecture

The package is structured as follows:

  • src/index.ts — Public exports: theme types, defaults, provider, and utilities
  • tailwind.config.js — Base Tailwind preset (CSS-variable-backed tokens); exported directly from the package root via the exports map, resolving to dist/tailwind.config.js
  • src/lib/theme.types.tsTheme, ThemeMode, ThemeContextValue, ColorPalette, and related interfaces
  • src/lib/default-theme.tsdefaultLightTheme and defaultDarkTheme objects
  • src/lib/theme-provider.tsxThemeProvider React component and useTheme hook
  • src/lib/theme-utils.tsgenerateCSSVariables, applyThemeToDOM, mergeThemes, saveThemeMode, loadThemeMode

Theme flow

  1. ThemeProvider initializes with defaultLightTheme and checks localStorage for a saved mode
  2. On mount and on every theme change, applyThemeToDOM(theme) sets CSS variables on :root
  3. When mode changes, ThemeProvider switches between defaultLightTheme and defaultDarkTheme (unless a custom initialTheme was passed)
  4. All Tailwind classes and direct var() CSS references update instantly
  5. saveThemeMode persists the selected mode so it survives page reloads

Best Practices

  1. Use semantic color tokens (primary-500, error-700) rather than hardcoded hex values
  2. Always include dark mode variants for components that use theme colors: text-neutral-900 dark:text-neutral-100
  3. Pass the Theme object from useTheme() directly to DataGrid's theme prop — it accepts the UITheme subset
  4. Call setTheme for brand overrides rather than replacing the entire theme — it deep-merges with the current values
  5. Do not use Tailwind dark: variants based on prefers-color-scheme; use the mode state from useTheme() and apply the dark theme object through ThemeProvider instead

Testing

nx test shared-frontend-ui-theme