Skip to main content
Soft UI’s theming system is built on CSS variables and data attributes, allowing live theme switching without page reload.

Theme Attributes

Themes are controlled by four HTML data attributes:
<html 
  data-mode="light" 
  data-scheme="mono" 
  data-theme-color="blue" 
  data-base-color="neutral"
>

data-mode

Controls light/dark mode:
  • light: Light mode
  • dark: Dark mode
document.documentElement.dataset.mode = 'dark';
Mode affects semantic tokens like --color-surface-page and --color-content-strong, adapting them for optimal contrast.

data-scheme

Controls whether primary actions use neutral or theme colors:
  • mono: Primary actions use neutral colors (grayscale)
  • color: Primary actions use theme colors
document.documentElement.dataset.scheme = 'color';
Mono scheme:
/* Primary button uses base colors */
--actions-primary-default: var(--base-800);
--actions-primary-hover: var(--base-900);
Color scheme:
/* Primary button uses theme colors */
--actions-primary-default: var(--theme-600);
--actions-primary-hover: var(--theme-700);
Use mono for professional/enterprise interfaces, color for consumer/creative products.

data-theme-color

Sets the accent color palette: Available colors: red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue (default), indigo, violet, purple, fuchsia, pink, rose, mauve, mist, olive, taupe
document.documentElement.dataset.themeColor = 'violet';
This remaps all --theme-* variables to the selected palette:
html[data-theme-color="violet"] {
  --theme-50: var(--violet-50);
  --theme-100: var(--violet-100);
  --theme-200: var(--violet-200);
  /* ... through --theme-950 */
}

data-base-color

Sets the neutral/grayscale palette: Available colors: neutral (default), slate, gray, zinc, stone, mauve, mist, olive, taupe
document.documentElement.dataset.baseColor = 'slate';
This remaps all --base-* variables:
html[data-base-color="slate"] {
  --base-50: var(--slate-50);
  --base-100: var(--slate-100);
  /* ... through --base-950 */
}
Neutral grays (mauve, mist, olive, taupe) are custom palettes designed for Soft UI with subtle tints.

Theme Initialization

Use createThemeInitScript from @soft-ui/tokens to prevent flash of unstyled content:
import { createThemeInitScript } from '@soft-ui/tokens';

export default function RootLayout({ children }) {
  return (
    <html suppressHydrationWarning>
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: createThemeInitScript({
              defaults: {
                defaultMode: 'light',
                defaultScheme: 'mono',
                defaultThemeColor: 'blue',
                defaultBaseColor: 'neutral',
              },
              storageKeys: {
                mode: 'ds-mode',
                scheme: 'ds-scheme',
                theme: 'ds-theme-color',
                base: 'ds-base-color',
              },
            }),
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}
The init script runs before React hydration, reading values from localStorage and applying them immediately.

Persisting Theme State

Theme preferences are stored in localStorage with configurable keys:
const storageKeys = {
  mode: 'ds-mode',           // light | dark
  scheme: 'ds-scheme',       // mono | color
  theme: 'ds-theme-color',   // blue | violet | etc.
  base: 'ds-base-color',     // neutral | slate | etc.
};
The init script:
  1. Reads stored values from localStorage
  2. Falls back to defaults if not found
  3. Respects system preference for mode (if no override)
  4. Applies values to data-* attributes

System Preference Detection

The init script detects system dark mode preference:
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
If no mode override is stored, it follows system preference and updates automatically when system preference changes.

Theme Switching

Manual Switching

function toggleMode() {
  const html = document.documentElement;
  const newMode = html.dataset.mode === 'light' ? 'dark' : 'light';
  html.dataset.mode = newMode;
  localStorage.setItem('ds-mode', newMode);
}

function toggleScheme() {
  const html = document.documentElement;
  const newScheme = html.dataset.scheme === 'mono' ? 'color' : 'mono';
  html.dataset.scheme = newScheme;
  localStorage.setItem('ds-scheme', newScheme);
}

function setThemeColor(color: string) {
  document.documentElement.dataset.themeColor = color;
  localStorage.setItem('ds-theme-color', color);
}

function setBaseColor(color: string) {
  document.documentElement.dataset.baseColor = color;
  localStorage.setItem('ds-base-color', color);
}

React Hook Example

import { useEffect, useState } from 'react';

type Mode = 'light' | 'dark';
type Scheme = 'mono' | 'color';

export function useTheme() {
  const [mode, setModeState] = useState<Mode>('light');
  const [scheme, setSchemeState] = useState<Scheme>('mono');

  useEffect(() => {
    const html = document.documentElement;
    setModeState(html.dataset.mode as Mode);
    setSchemeState(html.dataset.scheme as Scheme);
  }, []);

  const setMode = (newMode: Mode) => {
    document.documentElement.dataset.mode = newMode;
    localStorage.setItem('ds-mode', newMode);
    setModeState(newMode);
  };

  const setScheme = (newScheme: Scheme) => {
    document.documentElement.dataset.scheme = newScheme;
    localStorage.setItem('ds-scheme', newScheme);
    setSchemeState(newScheme);
  };

  return { mode, setMode, scheme, setScheme };
}

Theme Combinations

Common Configurations

Professional/Enterprise:
data-mode="light"
data-scheme="mono"
data-theme-color="blue"
data-base-color="neutral"
Creative/Consumer:
data-mode="light"
data-scheme="color"
data-theme-color="violet"
data-base-color="mauve"
Developer Tools:
data-mode="dark"
data-scheme="color"
data-theme-color="cyan"
data-base-color="slate"
Warm & Friendly:
data-mode="light"
data-scheme="color"
data-theme-color="amber"
data-base-color="taupe"

How Tokens Adapt

Surface Colors

Light mode:
--surface-page: var(--pure-white);
--surface-canvas: var(--base-50);
--surface-overlay: var(--pure-white);
Dark mode:
--surface-page: var(--base-900);
--surface-canvas: var(--darken-16);
--surface-overlay: var(--base-800);

Content Colors

Light mode:
--content-strong: var(--base-800);
--content-subtle: var(--base-500);
--content-muted: var(--base-400);
Dark mode:
--content-strong: var(--base-200);
--content-subtle: var(--base-400);
--content-muted: var(--base-500);

Action Colors (Scheme-Dependent)

Mono scheme (light):
--actions-primary-default: var(--base-800);
--actions-primary-hover: var(--base-900);
Color scheme (light):
--actions-primary-default: var(--theme-600);
--actions-primary-hover: var(--theme-700);
Mono scheme (dark):
--actions-primary-default: var(--base-200);
--actions-primary-hover: var(--base-100);
Color scheme (dark):
--actions-primary-default: var(--theme-500);
--actions-primary-hover: var(--theme-400);

Danger Color Handling

When theme color is set to rose, danger actions switch to red to maintain distinction:
html[data-theme-color="rose"] {
  --danger-50: var(--red-50);
  --danger-100: var(--red-100);
  /* ... through --danger-950 */
}
Otherwise, danger uses rose:
--danger-50: var(--rose-50);
--danger-100: var(--rose-100);
/* ... */

Best Practices

Do:
  • Use semantic tokens (--color-surface-page) instead of raw palette tokens (--blue-500)
  • Test both light and dark modes
  • Test both mono and color schemes
  • Provide theme controls in settings/preferences
Don’t:
  • Hardcode colors (use tokens)
  • Assume a specific theme color is set
  • Forget to persist theme state to localStorage
  • Skip the init script (causes flash of unstyled content)

Extending the Theme

To add a custom theme color:
  1. Define the palette in tokens.css:
--custom-50: 250 245 255;
--custom-100: 243 232 255;
/* ... through --custom-950 */
  1. Add a theme mapping:
html[data-theme-color="custom"] {
  --theme-50: var(--custom-50);
  --theme-100: var(--custom-100);
  /* ... through --theme-950 */
}
  1. Update TypeScript types in theme.ts:
export const themeColors = [
  // existing colors...
  "custom",
] as const;

Build docs developers (and LLMs) love