Skip to main content

Interactive State Color Theory

UI elements exist in discrete states, each requiring distinct visual feedback. The primary states form a progression of color intensity.
                    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

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
This establishes:
  • 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.
/* 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

                    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

/* 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;
These fallbacks provide:
  • 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.”
┌───────────────────────────────────────────────────────────────────────┐
│  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)        │
│                                                                       │
└───────────────────────────────────────────────────────────────────────┘
/**
 * 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

                    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:
  1. Reduced chroma (desaturation)
  2. Reduced contrast (closer to background)
  3. Grayed appearance (universal “inactive” signal)

Disabled State Pattern

                    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

┌───────────────────────────────────────────────────────────────────────┐
│  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)              │
│                                                                       │
└───────────────────────────────────────────────────────────────────────┘
/**
 * 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

               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

/* 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

               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.
[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

           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

               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

Connection to Core-Flow:
+-- States represent information "focus levels"
+-- Hover = approaching interaction
+-- Active = engaged interaction
+-- Disabled = blocked interaction
+-- Color changes guide user attention flow
State changes are the most immediate form of information flow feedback. When a user moves their cursor over a button, the hover state color change says “this element responds to you.” When they click, the active state darkening says “action registered.” When an element is disabled, the grayed appearance says “not available.” This creates a visual language where:
  • Lighter colors = approaching, available, inviting
  • Darker colors = pressed, confirmed, committed
  • Grayer colors = unavailable, blocked, inactive
The color “flows” through these states in response to user intention, creating a feedback loop that feels natural and responsive.

Muted Colors and Variants

Learn how muted colors create subtle backgrounds and contextual relationships