Interactive State Color Theory
UI elements exist in discrete states, each requiring distinct visual feedback. The primary states form a progression of color intensity.Copy
┌─────────────────────────────────────────────────────────────────────────────────┐
│ INTERACTIVE STATE MACHINE │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ mouse enter ┌─────────┐ mouse down ┌──────────┐ │
│ │ DEFAULT │ ──────────────────→ │ HOVER │ ─────────────────→│ ACTIVE │ │
│ │ STATE │ ←────────────────── │ STATE │ ←─────────────────│ STATE │ │
│ └─────────┘ mouse leave └─────────┘ mouse up └──────────┘ │
│ │ │ │ │
│ │ │ │ │
│ │ click │ │ │
│ │ ┌──────────────────────┴──────────────────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────┐ │
│ │ │ SELECTED│ (toggle state) │
│ │ │ STATE │ │
│ │ └─────────┘ │
│ │ │ │
│ │ │ (can still have hover/active when selected) │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────┐ │
│ └──→│ DISABLED │ (blocks all other states) │
│ │ STATE │ │
│ └─────────────┘ │
│ │
│ COLOR INTENSITY PROGRESSION: │
│ │
│ Default --> Hover --> Active --> Selected --> Disabled │
│ Base +Light +Dark +Tint -Chroma │
│ 100% +15% +10% +20% tint 60% gray │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Flow Color State Derivation Principle
Copy
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STATE COLOR DERIVATION TABLE │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ State │ Color Definition │ Direction │
│ ─────────────┼─────────────────────────────────────────────┼──────────────── │
│ default │ oklch(55% 0.15 H) │ Baseline │
│ :hover │ color-mix(in oklch, base, white 15%) │ Lighter │
│ :active │ color-mix(in oklab, base, black 20%) │ Darker │
│ :disabled │ color-mix(in oklab, base, gray 60%) │ Desaturated │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
- Hover: OKLCH adjustment (preserve hue, increase lightness)
- Active/Disabled: OKLAB mixing (perceptual color blending)
Hover State Implementation
Hover states make elements lighter to indicate interactivity. The color “approaches” brightness.Copy
/* Derived colors using color-mix() */
--oklch-primary-hover: color-mix(in oklch, var(--oklch-primary), white 15%);
--oklch-success-hover: color-mix(in oklch, var(--oklch-success), white 15%);
--oklch-danger-hover: color-mix(in oklch, var(--oklch-danger), white 15%);
--oklch-warning-hover: color-mix(in oklch, var(--oklch-warning), white 10%);
Hover State Color Analysis
Copy
┌─────────────────────────────────────────────────────────────────────────────────┐
│ HOVER STATE COLOR DERIVATION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ PATTERN: color-mix(in oklch, CORE, white N%) │
│ │
│ Why OKLCH (not OKLAB)? │
│ ═══════════════════════ │
│ │
│ 1. HUE PRESERVATION: OKLCH mixing with white preserves hue angle │
│ 2. PREDICTABLE LIGHTENING: L value increases proportionally │
│ 3. NO CHROMA SHIFT: Maintains saturation while lightening │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ PRIMARY HOVER ANALYSIS: │
│ ═══════════════════════ │
│ │
│ Base: --oklch-primary = oklch(55% 0.15 230) │
│ Hover: color-mix(in oklch, ..., white 15%) │
│ │
│ Result: oklch(~63% 0.13 230) │
│ ├──L increased ├──C slightly reduced └──H preserved │
│ │
│ ┌───────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ DEFAULT (55% L) HOVER (~63% L) │ │
│ │ #################### ....############.... │ │
│ │ Baseline Lighter │ │
│ │ (resting state) (interactive feedback) │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ WARNING HOVER EXCEPTION: │
│ ════════════════════════ │
│ │
│ --oklch-warning-hover: color-mix(in oklch, var(--oklch-warning), white 10%); │
│ │
│ WARNING uses 10% instead of 15% because: │
│ • Warning (oklch 65% 0.15 85) already has higher lightness │
│ • Adding 15% white would make it too light/washed out │
│ • Yellow/orange hues need less adjustment to appear "lighter" │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Legacy Hex Hover Fallbacks
Copy
/* Legacy HEX fallbacks (for older browser support) */
--color-primary: #2380c7;
--color-primary-hover: #268bd2;
--color-success: #7a8c00;
--color-success-hover: #859900;
--color-danger: #c42e2b;
--color-danger-hover: #dc322f;
--color-warning: #a37a00;
--color-warning-hover: #b58900;
- Browser compatibility for systems without
color-mix()support - Explicit hex values matching the intended OKLCH hover result
- Documentation of target color values
Active State Derivation
Active states (mouse down, pressed) use darkening rather than lightening. This creates the illusion of the element being “pressed in.”Copy
┌─────────────────────────────────────────────────────────────────────────────────┐
│ COLORDARKER FUNCTION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ INPUT: color, amount (default: 30) │
│ │
│ FORMULA: colorMix(color, "black", "oklab", 100 - amount) │
│ │
│ EXAMPLE: colorDarker("var(--primary)", 30) │
│ = colorMix("var(--primary)", "black", "oklab", 70) │
│ = "color-mix(in oklab, var(--primary) 70%, black)" │
│ │
│ RESULT: 70% original color + 30% black (perceptually uniform) │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Copy
/**
* Generate darker variant using color-mix
* Uses oklab for perceptually uniform darkening
*/
export function colorDarker(color: string, amount: number = 30): string {
return colorMix(color, "black", "oklab", 100 - amount);
}
Active State Pattern
Copy
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ACTIVE STATE DERIVATION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ PATTERN: color-mix(in oklab, CORE, black N%) │
│ │
│ Why OKLAB (not OKLCH)? │
│ ═══════════════════════ │
│ │
│ 1. PERCEPTUAL DARKENING: OKLAB provides uniform perceived darkness │
│ 2. NO HUE ROTATION: Mixing with black in OKLAB preserves hue better │
│ 3. PREDICTABLE CONTRAST: Linear relationship with added darkness │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ EXAMPLE: Primary Active State │
│ ══════════════════════════════ │
│ │
│ Default: oklch(55% 0.15 230) │
│ Active: color-mix(in oklab, var(--oklch-primary), black 20%) │
│ │
│ This creates approximately: │
│ oklch(~44% 0.12 230) - 20% darker with slightly reduced chroma │
│ │
│ ┌───────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ STATE PROGRESSION: │ │
│ │ │ │
│ │ HOVER DEFAULT ACTIVE │ │
│ │ ....####.... ############ ====####==== │ │
│ │ (lighter) (base) (darker) │ │
│ │ ~63% L 55% L ~44% L │ │
│ │ │ │
│ │ │ │
│ │ ^ +15% white baseline ▼ +20% black │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
│ │
│ IMPLEMENTATION NOTE: │
│ The codebase does not define explicit --*-active variables. │
│ Active states are typically applied directly in component CSS: │
│ │
│ .button:active { │
│ background: color-mix(in oklab, var(--button-bg), black 20%); │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Disabled State Derivation
Disabled states communicate non-interactivity through:- Reduced chroma (desaturation)
- Reduced contrast (closer to background)
- Grayed appearance (universal “inactive” signal)
Disabled State Pattern
Copy
┌─────────────────────────────────────────────────────────────────────────────────┐
│ DISABLED STATE DERIVATION │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ PATTERN: color-mix(in oklab, CORE, gray 60%) │
│ │
│ Why OKLAB with gray? │
│ ═════════════════════ │
│ │
│ 1. PERCEPTUAL DESATURATION: Mixing with gray reduces chroma uniformly │
│ 2. HUE HINT PRESERVED: Original color is still slightly visible │
│ 3. UNIVERSAL SIGNAL: Gray = inactive in UI convention │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ EXAMPLE: Primary Disabled State │
│ ════════════════════════════════ │
│ │
│ Default: oklch(55% 0.15 230) - Vibrant blue │
│ Disabled: color-mix(in oklab, var(--oklch-primary), gray 60%) │
│ │
│ Result: A grayish-blue that retains color identity but signals inactivity │
│ │
│ ┌───────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ENABLED DISABLED │ │
│ │ ######################## ======================== │ │
│ │ 100% chroma 40% chroma (60% gray) │ │
│ │ Full saturation Desaturated │ │
│ │ Interactive Non-interactive │ │
│ │ │ │
│ │ cursor: pointer cursor: not-allowed │ │
│ │ opacity: 1 opacity: 0.7 (optional) │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
│ │
│ COMPLEMENTARY TECHNIQUES: │
│ ══════════════════════════ │
│ │
│ Disabled states often combine: │
│ 1. color-mix() for color │
│ 2. opacity: 0.6-0.8 for additional dimming │
│ 3. cursor: not-allowed for interaction feedback │
│ 4. pointer-events: none (optional) for click blocking │
│ │
│ .button:disabled { │
│ background: color-mix(in oklab, var(--button-bg), gray 60%); │
│ color: color-mix(in oklab, var(--button-text), gray 60%); │
│ opacity: 0.7; │
│ cursor: not-allowed; │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
TypeScript State Color Utilities
Copy
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STATE COLOR UTILITY FUNCTIONS │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ colorDarker(color, amount=30) │
│ │ │
│ └──► colorMix(color, "black", "oklab", 100-amount) │
│ = color-mix(in oklab, {color} 70%, black) │
│ │
│ colorLighter(color, amount=30) │
│ │ │
│ └──► colorMix(color, "white", "oklab", 100-amount) │
│ = color-mix(in oklab, {color} 70%, white) │
│ │
│ NOTE: Both use OKLAB for perceptually uniform adjustments │
│ (CSS hover states use OKLCH for hue preservation) │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Copy
/**
* Generate darker variant using color-mix
* Uses oklab for perceptually uniform darkening
*/
export function colorDarker(color: string, amount: number = 30): string {
return colorMix(color, "black", "oklab", 100 - amount);
}
/**
* Generate lighter variant using color-mix
* Uses oklab for perceptually uniform lightening
*/
export function colorLighter(color: string, amount: number = 30): string {
return colorMix(color, "white", "oklab", 100 - amount);
}
Utility Function Analysis
Copy
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STATE COLOR UTILITY FUNCTIONS │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ FUNCTION: colorDarker(color, amount) │
│ ═════════════════════════════════════ │
│ │
│ Input: color = "var(--oklch-primary)" │
│ amount = 30 (default) │
│ │
│ Output: "color-mix(in oklab, var(--oklch-primary) 70%, black)" │
│ │
│ Logic: │
│ • colorMix(color, "black", "oklab", 100 - amount) │
│ • 100 - 30 = 70% of original color, 30% black │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ FUNCTION: colorLighter(color, amount) │
│ ══════════════════════════════════════ │
│ │
│ Input: color = "var(--oklch-primary)" │
│ amount = 30 (default) │
│ │
│ Output: "color-mix(in oklab, var(--oklch-primary) 70%, white)" │
│ │
│ Logic: │
│ • colorMix(color, "white", "oklab", 100 - amount) │
│ • 100 - 30 = 70% of original color, 30% white │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ NOTE ON COLOR SPACE CHOICE: │
│ ════════════════════════════ │
│ │
│ Both functions use OKLAB, NOT OKLCH │
│ │
│ This differs from the CSS variable hover states which use OKLCH! │
│ │
│ CSS: --oklch-primary-hover: color-mix(in oklch, ..., white 15%); │
│ TS: colorLighter(color) -> color-mix(in oklab, ..., white) │
│ │
│ The TypeScript utilities prioritize perceptual uniformity (OKLAB) │
│ while CSS variables prioritize hue preservation (OKLCH). │
│ │
│ This is a DESIGN DECISION, not inconsistency: │
│ • OKLCH for identity operations (hover preserves hue) │
│ • OKLAB for general operations (active, disabled) │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
State Color Consistency Across Semantic Colors
Copy
/* Primary Colors in OKLCH */
--oklch-primary: oklch(55% 0.15 230); /* Blue */
--oklch-success: oklch(55% 0.15 130); /* Green */
--oklch-danger: oklch(55% 0.18 25); /* Red */
--oklch-warning: oklch(65% 0.15 85); /* Yellow/Orange */
--oklch-info: oklch(55% 0.12 230); /* Blue (softer) */
/* Derived colors using color-mix() */
--oklch-primary-hover: color-mix(in oklch, var(--oklch-primary), white 15%);
--oklch-primary-muted: color-mix(in oklab, var(--oklch-primary), transparent 80%);
--oklch-success-hover: color-mix(in oklch, var(--oklch-success), white 15%);
--oklch-success-muted: color-mix(in oklab, var(--oklch-success), transparent 80%);
--oklch-danger-hover: color-mix(in oklch, var(--oklch-danger), white 15%);
--oklch-danger-muted: color-mix(in oklab, var(--oklch-danger), transparent 80%);
--oklch-warning-hover: color-mix(in oklch, var(--oklch-warning), white 10%);
--oklch-warning-muted: color-mix(in oklab, var(--oklch-warning), transparent 80%);
State Derivation Consistency
Copy
┌─────────────────────────────────────────────────────────────────────────────────┐
│ SEMANTIC COLOR STATE CONSISTENCY │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ COLOR │ BASE OKLCH │ HOVER FORMULA │ MUTED FORMULA │
│ ═════════════════════════════════════════════════════════════════════════════ │
│ │
│ primary │ oklch(55% 0.15 230) │ color-mix(oklch, │ color-mix(oklab, │
│ │ │ ..., white 15%) │ ..., transparent 80%)│
│ │
│ success │ oklch(55% 0.15 130) │ color-mix(oklch, │ color-mix(oklab, │
│ │ │ ..., white 15%) │ ..., transparent 80%)│
│ │
│ danger │ oklch(55% 0.18 25) │ color-mix(oklch, │ color-mix(oklab, │
│ │ │ ..., white 15%) │ ..., transparent 80%)│
│ │
│ warning │ oklch(65% 0.15 85) │ color-mix(oklch, │ color-mix(oklab, │
│ │ │ ..., white 10%) <-- │ ..., transparent 80%)│
│ │ │ EXCEPTION! │ │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ PATTERN CONSISTENCY: │
│ ════════════════════ │
│ │
│ HOVER: All use OKLCH with white (except warning uses 10% instead of 15%) │
│ MUTED: All use OKLAB with transparent 80% │
│ │
│ This consistency ensures: │
│ 1. Uniform visual weight across semantic categories │
│ 2. Predictable behavior for designers │
│ 3. Easy pattern replication for custom colors │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Theme-Specific State Adjustments
Light themes require inverted state logic because darker elements provide better feedback on light backgrounds.Copy
[data-theme="light"] {
/* Semantic Colors - Light Theme */
--color-primary: #0066cc;
--color-primary-hover: #0055aa; /* <-- DARKER, not lighter! */
--color-success: #118844;
--color-success-hover: #23aa55;
--color-danger: #c42e2b;
--color-danger-hover: #e04040;
--color-warning: #a37a00;
--color-warning-hover: #c89800;
}
Light vs Dark Theme State Comparison
Copy
┌─────────────────────────────────────────────────────────────────────────────────┐
│ THEME-SPECIFIC STATE COLOR COMPARISON │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ PRIMARY COLOR STATES: │
│ ═════════════════════ │
│ │
│ State Dark Theme Light Theme │
│ ─────────────────────────────────────────────────────────────────────────── │
│ base #2380c7 #0066cc │
│ hover #268bd2 #0055aa <-- INVERTED! │
│ │
│ OBSERVATION: │
│ ───────────── │
│ Dark theme: hover is LIGHTER than base (#268bd2 > #2380c7) │
│ Light theme: hover is DARKER than base (#0055aa < #0066cc) │
│ │
│ This inversion is intentional: │
│ • Dark theme: lighter = more visible = better feedback │
│ • Light theme: darker = more contrast = better feedback │
│ │
│ ─────────────────────────────────────────────────────────────────────────── │
│ │
│ SUCCESS COLOR STATES: │
│ ═════════════════════ │
│ │
│ State Dark Theme Light Theme │
│ ─────────────────────────────────────────────────────────────────────────── │
│ base #7a8c00 #118844 │
│ hover #859900 #23aa55 │
│ │
│ Light theme success is noticeably different: │
│ • Dark: Yellow-green (#7a8c00) │
│ • Light: Pure green (#118844) │
│ • This is a DESIGN CHOICE for optimal visibility on each background │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Color Space Selection Rules
Copy
┌─────────────────────────────────────────────────────────────────────────────────┐
│ STATE COLOR SPACE SELECTION RULES │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ USE OKLCH FOR: │
│ ══════════════ │
│ │
│ • Hover states with white (identity preservation) │
│ • Any adjustment where HUE must be preserved exactly │
│ • Lightness adjustments on identity colors │
│ │
│ USE OKLAB FOR: │
│ ══════════════ │
│ │
│ • Active states with black │
│ • Disabled states with gray │
│ • Transparency mixing │
│ • Any blending operation between two colors │
│ • Perceptually uniform adjustments │
│ │
│ MNEMONIC: │
│ ══════════ │
│ │
│ "OKLCH for Identity (hover), OKLAB for Blending (everything else)" │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
THE CENTER
How State Changes Visualize Information Flow
Copy
Connection to Core-Flow:
├── States represent information "focus levels"
├── Hover = approaching interaction
├── Active = engaged interaction
├── Disabled = blocked interaction
└── Color changes guide user attention flow
- Lighter colors = approaching, available, inviting
- Darker colors = pressed, confirmed, committed
- Grayer colors = unavailable, blocked, inactive
Muted Colors and Variants
Learn how muted colors create subtle backgrounds and contextual relationships