---
name: color-systems
description: Build perceptually uniform color systems in OKLCH — from primitives through semantic roles to dark-mode-safe themes that pass WCAG contrast.
---

# Color Systems

OKLCH palettes, semantic roles, and contrast-safe themes

Build color systems that are perceptually uniform, accessible, and themeable. Work in OKLCH — it matches how humans see lightness and color, unlike hex or HSL.

## Why OKLCH

- **Perceptual uniformity**: equal lightness steps look equally different. L 0.9 to L 0.8 feels the same jump as L 0.5 to L 0.4.
- **Polar coordinates**: hue is a degree (0–360), chroma is saturation (0–0.4 typical), lightness is 0–1.
- **No gamut clipping**: unlike HSL, OKLCH colors that look the same brightness actually ARE the same brightness.
- **Better contrast prediction**: if two colors have ≥ 0.4 lightness difference, they almost certainly pass WCAG AA.

## The Primitive Palette

Build a scale from 50 to 950 for each hue. Keep chroma consistent within a hue family.

```css
/* Neutral scale (chroma 0.01–0.02, hue toward brand) */
--gray-50:  oklch(0.98 0.005 240);
--gray-100: oklch(0.96 0.008 240);
--gray-200: oklch(0.92 0.01 240);
--gray-300: oklch(0.87 0.012 240);
--gray-400: oklch(0.71 0.015 240);
--gray-500: oklch(0.55 0.02 240);
--gray-600: oklch(0.45 0.02 240);
--gray-700: oklch(0.37 0.02 240);
--gray-800: oklch(0.27 0.015 240);
--gray-900: oklch(0.2 0.01 240);
--gray-950: oklch(0.14 0.008 240);

/* Brand scale (chroma 0.12–0.2, consistent hue) */
--blue-50:  oklch(0.97 0.02 240);
--blue-100: oklch(0.93 0.04 240);
--blue-200: oklch(0.88 0.08 240);
--blue-300: oklch(0.8 0.12 240);
--blue-400: oklch(0.7 0.16 240);
--blue-500: oklch(0.55 0.2 240);   /* primary */
--blue-600: oklch(0.48 0.2 240);
--blue-700: oklch(0.4 0.18 240);
--blue-800: oklch(0.32 0.14 240);
--blue-900: oklch(0.25 0.1 240);
```

Rules:
- Keep hue consistent within a family (±5° max).
- Increase chroma toward the middle of the scale (300–500), decrease at extremes.
- Neutral scales: chroma 0.005–0.02. Tint toward brand hue, not toward warmth-by-default.

## The Semantic Layer

Map primitives to roles. This layer changes for dark mode; primitives stay.

```css
/* Foreground hierarchy */
--fg-primary: var(--gray-900);      /* headings, body text */
--fg-secondary: var(--gray-600);    /* descriptions, labels */
--fg-tertiary: var(--gray-400);     /* hints, placeholders */
--fg-muted: var(--gray-300);        /* disabled, decorative */

/* Background elevation */
--bg-base: var(--gray-50);          /* page background */
--bg-raised: var(--white);          /* cards, modals */
--bg-sunken: var(--gray-100);       /* inputs, code blocks */
--bg-overlay: oklch(0 0 0 / 0.5);  /* modal backdrop */

/* Border progression */
--border-subtle: var(--gray-200);   /* card borders, dividers */
--border-default: var(--gray-300);  /* input borders */
--border-strong: var(--gray-400);   /* focus rings */

/* Brand */
--color-primary: var(--blue-500);
--color-primary-hover: var(--blue-600);
--color-primary-active: var(--blue-700);
--color-primary-content: var(--white);

/* Semantic feedback */
--color-success: oklch(0.6 0.17 155);
--color-warning: oklch(0.75 0.16 85);
--color-error: oklch(0.6 0.2 25);
--color-info: oklch(0.6 0.15 240);
```

## Dark Mode

Swap semantics, keep primitives:

```css
[data-theme="dark"] {
  --fg-primary: var(--gray-100);
  --fg-secondary: var(--gray-400);
  --fg-tertiary: var(--gray-500);
  --bg-base: var(--gray-950);
  --bg-raised: var(--gray-900);
  --bg-sunken: var(--gray-800);
  --border-subtle: var(--gray-800);
  --border-default: var(--gray-700);
  --color-primary: var(--blue-400);
  --color-primary-hover: var(--blue-300);
  --color-primary-content: var(--gray-950);
}
```

Rules:
- Dark mode primary is often 1–2 steps lighter than light mode (better contrast on dark bg).
- Semantic colors (success/warning/error) often need desaturation in dark mode to avoid vibrating.
- `color-scheme: dark;` is mandatory for PageWeave dark themes.

## Contrast Verification

The fast OKLCH contrast check: **lightness difference ≥ 0.4 almost always passes WCAG AA (4.5:1)**.

For precise checks:
- Body text (normal): ≥ 4.5:1 against background.
- Large text (≥ 18px or bold ≥ 14px): ≥ 3:1.
- UI components: ≥ 3:1 against adjacent colors.
- Focus indicators: ≥ 3:1 against both background and adjacent colors.

Every `*-content` token must pass contrast against its parent:
- `--color-primary-content` against `--color-primary`.
- `--fg-primary` against `--bg-base` and `--bg-raised`.
- `--fg-secondary` against `--bg-base`.

## Color Usage Rules

1. **One accent color, used with intention.** One primary color carries action. Secondary and accent are for rare emphasis.
2. **Semantic colors are functional.** Success/warning/error appear in feedback contexts only — never decoration.
3. **Opacity modifiers over new colors.** `text-base-content/60` gives you a dimmed version that survives theming. Don't create `--text-dim` when opacity works.
4. **Never use color alone to convey information.** Pair with text, icon, or pattern.
5. **Test the full chain.** primary → primary-hover → primary-active → primary-content. Each step should be visibly distinct.

## Palette Anti-Patterns

- **Purple-blue gradient**: the most common AI color scheme. Avoid.
- **Single-hue palettes**: everything blue, or everything green. Use neutrals for structure, color for emphasis.
- **Rainbow palettes**: 6+ distinct hues in one interface. Stick to 1 brand hue + neutrals + semantic colors.
- **Hardcoded hex in components**: `bg-[#6366f1]` doesn't survive theming. Use `bg-primary`.
- **Warm-by-default neutrals**: tinting neutrals toward warmth "because the brand feels warm" is the monoculture move. Tint toward the brand's actual hue.
- **Similar lightness for text and background**: OKLCH L difference < 0.3 will fail contrast. Check before shipping.

## PageWeave Workflow

1. Define primitives and semantics in OKLCH.
2. Pass bare variables to `update_theme(website, config: { css })` — no `[data-theme]` wrapper.
3. Dark themes: include `color-scheme: dark;`.
4. Test: screenshot a page with buttons, badges, inputs, tables, and an alert — all five exercise semantic colors.
5. Verify contrast for every text/background pair.
6. Check `.md` view: code blocks inherit theme colors.