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 | |
| +----------------------+------------------------------+
| |
| v
| +---------+
| | SELECTED| (toggle state)
| | STATE |
| +---------+
| |
| | (can still have hover/active when selected)
| |
| v
| +-------------+
+-->| 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 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 v +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