Skip to content

/theme-create

Generate a full 18-role semantic color palette in OKLCH color space from a single hex color. Every role is WCAG 2.2 AA contrast-validated before the files are written.

/theme-create <hex-color> [--mode=light|dark|both]
ArgumentRequiredDefaultDescription
hex-colorYesSeed color as a 6-digit hex (e.g., #4f46e5)
--modeNobothWhich theme files to generate: light, dark, or both

Generate both light and dark themes from indigo:

/theme-create #4f46e5

Generate only the light theme:

/theme-create #4f46e5 --mode=light

Generate only the dark theme:

/theme-create #2563eb --mode=dark
FileContents
src/styles/theme/light.css:root block with all 18 --color-* custom properties
src/styles/theme/dark.css[data-theme="dark"] block with dark-mode values
RolePurpose
--color-backgroundPage background
--color-surfaceCard/panel background
--color-surface-raisedElevated surface (dropdowns, modals)
--color-borderDefault border
--color-border-strongEmphasized border
--color-textBody text
--color-text-inverseText on colored backgrounds
--color-text-mutedSecondary/caption text
--color-text-disabledDisabled state text
--color-primaryPrimary brand action
--color-primary-hoverPrimary hover state
--color-focus-ringKeyboard focus outline
--color-dangerDestructive / error state
--color-warningWarning state
--color-infoInformational state
--color-successSuccess / positive state
RolePurpose
--color-accentSecondary brand accent
--color-accent-hoverAccent hover state
--color-brand-accentDecorative brand color

The palette generation uses OKLCH color space for perceptually uniform scaling:

  1. Parse seed → convert #hex to OKLCH (L%, C, H°)
  2. Set primary → use seed hue as --color-primary; find highest lightness where contrast ≥ 4.5:1 against background
  3. Derive backgrounds → high lightness (L 97–99%) for light mode, low lightness (L 12–20%) for dark
  4. Derive text → lightness that achieves ≥ 4.5:1 against its paired background
  5. Semantic hues → danger at H°27, warning at H°80, success at H°155, info at H°240 — derived chroma from seed
  6. Hover statesprimary-hover = primary with L reduced by ~5 steps; focus-ring = primary with L raised by ~8 steps
  7. WCAG validation → check all 10 contrast pairs; adjust lightness if any fail; retry up to 8 times
ForegroundBackgroundMinimum
--color-text--color-background4.5:1
--color-primary--color-background4.5:1
--color-text-inverse--color-primary4.5:1
--color-danger--color-background4.5:1
--color-success--color-background4.5:1
--color-warning--color-background3.0:1
--color-info--color-background4.5:1
--color-text-muted--color-background4.5:1
--color-border--color-background3.0:1
--color-text--color-surface4.5:1

If a contrast pair cannot pass AA after 8 lightness iterations, the command:

  1. Reports the failing pair and its computed ratio
  2. Suggests an alternative seed color with sufficient contrast range
  3. Does not write any files (atomic — either all pass or nothing is written)

Dark mode values are written in [data-theme="dark"] selector blocks (not @media prefers-color-scheme). This gives you manual control over when dark mode activates:

[data-theme="dark"] {
--color-background: oklch(12% 0.008 270);
/* ... */
}

To activate dark mode, set data-theme="dark" on <html>:

document.documentElement.dataset.theme = 'dark';