Skip to content

Color Systems for Digital Products

OWNER: Christel (Brand Identity Designer) ALSO_USED_BY: Tobias (UI/UX Designer), Sandro (Front-End Developer), Boris (Senior Developer), Faye (Team Alfa PM), Sytske (Team Bravo PM) LAST_UPDATED: 2026-03-28 CONFIDENCE: High — critical for every digital product GE delivers


Why This Matters

Color is the most immediately visible element of any brand. A poorly built color system causes cascading problems: accessibility failures, dark mode that looks broken, inconsistent UI states, and developer frustration. Christel must build color systems that are not just beautiful, but engineered — systematic, accessible, and implementable by agents who work in code, not in design tools.


Color System Architecture

The Two-Layer Token Model

A production color system has two layers. This is non-negotiable for any system that needs to support theming (light/dark mode) or multiple brands.

Layer 1: PRIMITIVE TOKENS (also called "base" or "global" tokens)
         Raw color values. Named by what they ARE.

         blue-500: #1A73E8
         blue-600: #1557B0
         gray-100: #F5F5F5
         gray-900: #212121
         red-500:  #EA4335
         green-500: #34A853

Layer 2: SEMANTIC TOKENS (also called "alias" or "purpose" tokens)
         Named by what they DO. Reference primitive tokens.

         color-primary:        → blue-500
         color-primary-hover:  → blue-600
         color-background:     → gray-100
         color-text:           → gray-900
         color-error:          → red-500
         color-success:        → green-500

Why Two Layers?

Theming becomes trivial. To switch from light to dark mode, you only remap the semantic layer:

LIGHT MODE                          DARK MODE
color-background: → gray-100        color-background: → gray-900
color-text:       → gray-900        color-text:       → gray-100
color-surface:    → white            color-surface:    → gray-800
color-primary:    → blue-500         color-primary:    → blue-400

The primitive tokens never change. The semantic tokens point to different primitives per theme. Every component uses semantic tokens, so the entire UI re-themes automatically.

Multi-brand support. For GE clients who need multiple brands (sub-brands, white-label), the same pattern works: each brand maps semantic tokens to its own primitive palette.


Building a Primitive Color Palette

Step 1: Start from Brand Colors

The client's brand colors (defined in the logo/identity phase) become the anchors. Typically: - 1 primary brand color - 1–2 secondary brand colors - The rest is generated systematically

Step 2: Generate a Full Scale

For each brand color, generate a 10-step scale from near-white to near-black:

blue-50:   #E8F0FE  (tint — very light, backgrounds)
blue-100:  #D2E3FC
blue-200:  #AECBFA
blue-300:  #8AB4F8
blue-400:  #669DF6
blue-500:  #1A73E8  ← brand color (anchor)
blue-600:  #1557B0
blue-700:  #104D9A
blue-800:  #0D3F82
blue-900:  #0A2F61  (shade — very dark, text on light bg)

Palette Generation Techniques

  1. HSL manipulation (recommended starting point):
  2. Keep Hue constant (or shift by max 5 degrees across the scale).
  3. Decrease Saturation as you go lighter (tints should not be neon).
  4. Increase Lightness for tints, decrease for shades.
  5. The 50 step should be ~95-97% lightness. The 900 step should be ~15-20% lightness.

  6. Perceptual uniformity (advanced, recommended for production):

  7. HSL is not perceptually uniform — a 10% lightness change in yellow looks different from 10% in blue.
  8. Use OKLCH or CIELAB color spaces for perceptually even steps.
  9. OKLCH: oklch(L C H) — L is perceptual lightness (0–1), C is chroma, H is hue.
  10. Modern CSS supports oklch() natively. This is the future-proof approach.

  11. Bezier curve interpolation:

  12. Plot the anchor color on a curve from white to black.
  13. Interpolate intermediate steps along the curve.
  14. Tools: Huet, Leonardo (Adobe), Scale by Colorbox.

Neutral Palette

Every system needs a neutral (gray) scale. Two approaches:

  1. Pure gray: Neutral without hue. Clean, safe, works with any brand color.
  2. Warm/cool gray (recommended): Add a tiny amount of the brand hue to the gray scale. This makes neutrals feel cohesive with the brand.
  3. Warm: Add 2–5% saturation with a warm hue (30–50 degrees).
  4. Cool: Add 2–5% saturation with a cool hue (210–230 degrees).
  5. Brand-tinted: Add 2–5% of the primary brand color's hue.

Semantic Token Architecture

The Full Semantic Map

SURFACES
  color-background:         Main page background
  color-surface:            Card/container background
  color-surface-elevated:   Modal, dropdown, popover background
  color-surface-overlay:    Scrim/overlay (typically black at 50% opacity)
  color-surface-inverse:    Inverted surface (dark on light, light on dark)

TEXT
  color-text:               Primary body text
  color-text-secondary:     Secondary/muted text
  color-text-tertiary:      Placeholder, disabled text
  color-text-inverse:       Text on inverse surface
  color-text-on-primary:    Text on primary color (usually white)
  color-text-link:          Hyperlink text

BORDERS
  color-border:             Default border
  color-border-strong:      Emphasized border (input focus, dividers)
  color-border-subtle:      Subtle separator

INTERACTIVE (mapped from brand primary)
  color-primary:            Primary buttons, links, active states
  color-primary-hover:      Primary hover state
  color-primary-active:     Primary pressed/active state
  color-primary-subtle:     Light tint for primary backgrounds (tags, badges)

SEMANTIC STATES
  color-success:            Positive state (confirmation, complete)
  color-success-subtle:     Success background
  color-success-text:       Success text (dark enough for contrast)

  color-warning:            Caution state (attention needed)
  color-warning-subtle:     Warning background
  color-warning-text:       Warning text

  color-error:              Negative state (error, destructive)
  color-error-subtle:       Error background
  color-error-text:         Error text

  color-info:               Informational state
  color-info-subtle:        Info background
  color-info-text:          Info text

SPECIFIC COMPONENTS (optional — only if needed)
  color-input-background:   Form input background
  color-input-border:       Form input border
  color-input-focus:        Form input focus ring
  color-badge-background:   Badge/chip background
  color-tooltip-background: Tooltip background

Light/Dark Mode: How to Do It Right

Principles

  1. Dark mode is NOT an inversion. You cannot just swap black and white. Surfaces need careful layering, contrast ratios change, and brand colors often need adjustment.

  2. Elevation = lightness in dark mode. In light mode, shadows create depth. In dark mode, shadows are invisible — instead, higher surfaces are lighter.

LIGHT MODE SURFACES          DARK MODE SURFACES
background:  #FFFFFF          background:  #121212
surface:     #FFFFFF          surface:     #1E1E1E    (slightly lighter)
elevated:    #FFFFFF + shadow  elevated:    #2C2C2C   (lighter still)
  1. Brand colors may need adjustment. A saturated blue (#1A73E8) that looks great on white may vibrate painfully on black. Reduce saturation or increase lightness for dark mode.
LIGHT MODE                   DARK MODE
primary: blue-500 (#1A73E8)  primary: blue-400 (#669DF6)  ← lighter, less saturated
  1. Semantic state colors adjust too. Error red on dark backgrounds needs to be a lighter red. Success green needs to be lighter. The "subtle" backgrounds (e.g., light red for error states) become dark tints instead.

Dark Mode Token Mapping Example

/* Light theme */
:root {
  --color-background: #FFFFFF;
  --color-surface: #FFFFFF;
  --color-surface-elevated: #FFFFFF;
  --color-text: #212121;
  --color-text-secondary: #616161;
  --color-text-tertiary: #9E9E9E;
  --color-border: #E0E0E0;
  --color-primary: #1A73E8;
  --color-primary-hover: #1557B0;
  --color-primary-subtle: #E8F0FE;
  --color-error: #EA4335;
  --color-error-subtle: #FCE8E6;
}

/* Dark theme */
[data-theme="dark"] {
  --color-background: #121212;
  --color-surface: #1E1E1E;
  --color-surface-elevated: #2C2C2C;
  --color-text: #E0E0E0;
  --color-text-secondary: #9E9E9E;
  --color-text-tertiary: #757575;
  --color-border: #424242;
  --color-primary: #669DF6;
  --color-primary-hover: #8AB4F8;
  --color-primary-subtle: #1A2744;
  --color-error: #F28B82;
  --color-error-subtle: #3C1F1E;
}

System Preference Detection

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* dark mode tokens */
  }
}

This respects system preference but allows manual override via data-theme attribute.


Accessibility: Non-Negotiable Requirements

WCAG Contrast Requirements

Element WCAG AA (minimum) WCAG AAA (enhanced)
Normal text (<18px or <14px bold) 4.5:1 7:1
Large text (>=18px or >=14px bold) 3:1 4.5:1
UI components & graphical objects 3:1 Not defined
Focus indicators 3:1 Not defined

GE standard: WCAG AA minimum for ALL text. WCAG AAA target for body text.

How to Verify Contrast

For every text/background combination in the system, calculate the contrast ratio:

Contrast ratio = (L1 + 0.05) / (L2 + 0.05)
Where L1 = relative luminance of the lighter color
      L2 = relative luminance of the darker color

Tools: WebAIM Contrast Checker, Stark (Figma plugin), axe DevTools.

Common Contrast Failures

Combination Typical Ratio Passes AA? Fix
Light gray text on white 2:1 – 3:1 NO Darken the gray (use gray-600 minimum on white)
White text on yellow 1.5:1 – 2.5:1 NO Never use white on yellow. Use dark text instead.
Brand blue on dark blue 2:1 – 3:1 MAYBE Test specifically. Often fails. Use light text on dark blue.
Colored text on colored background Varies TEST Always verify. Color-on-color is the highest risk for failure.

Color Blindness Considerations

Approximately 8% of men and 0.5% of women have some form of color vision deficiency.

Types: | Type | Prevalence | Confusion | Impact | |------|-----------|-----------|--------| | Deuteranomaly (reduced green) | 5% of males | Red ↔ Green | Most common; red/green buttons look similar | | Protanomaly (reduced red) | 1.3% of males | Red ↔ Green | Similar to above, reds appear darker | | Tritanomaly (reduced blue) | 0.01% | Blue ↔ Yellow | Rare but impacts blue-heavy palettes | | Achromatopsia (no color) | 0.003% | All colors | Extremely rare; rely on luminance contrast |

Design rules: 1. Never use color ALONE to convey information. Always pair with: an icon, a label, a pattern, or a positional cue. 2. Red/green is the most dangerous pair. For success/error states, also use icons (checkmark / X) and text labels. 3. Test with simulation. Use Sim Daltonism, Figma color blindness plugin, or Chrome DevTools (Rendering > Emulate vision deficiencies). 4. If a chart uses color to distinguish categories, add patterns or labels. Bar charts should have labels on or next to each bar, not just a color legend.


Brand Colors to UI Colors: The Translation

When a brand has a primary blue (#1A73E8), how does it become a full UI color system?

The Mapping

BRAND COLOR                 UI ROLE
─────────────────────────   ──────────────────────────────
Primary brand color    →    Primary interactive color
                            (buttons, links, focus rings,
                            active indicators, selected states)

                       →    Primary subtle backgrounds
                            (selected list item, active tab bg,
                            info banners)

Secondary brand color  →    Accent/secondary interactive
                            (secondary buttons, toggles,
                            progress indicators)

                       →    Decorative elements
                            (illustrations, brand moments,
                            empty states)

Neutral palette        →    Text, backgrounds, borders,
                            dividers, disabled states
                            (this is 80%+ of the UI)

NOT from brand colors  →    Semantic states
                            (success=green, warning=amber,
                            error=red, info=blue)
                            These are UNIVERSAL. Do not rebrand them.

Critical Rule: Semantic Colors Override Brand Colors

If the brand's primary color is red, you CANNOT use red for primary buttons — because red means "error" or "destructive" in UI conventions. Options: 1. Use the secondary brand color for primary interactive elements. 2. Use the brand red only for brand moments (hero sections, marketing) and a neutral/blue for UI interactives. 3. Discuss with the client — this is a genuine constraint that must be resolved at the brand level.

Similarly: if the brand color is green, primary buttons will be confused with "success" states. Address this explicitly.


Palette Generation: Step-by-Step for GE

Input

  • Primary brand color (from Christel's identity work)
  • Secondary brand color (optional)
  • Brand personality (warm/cool/neutral)

Process

  1. Generate primary scale (10 steps) using OKLCH or HSL manipulation.
  2. Generate secondary scale (10 steps) if a secondary brand color exists.
  3. Generate neutral scale (10 steps) tinted with the primary hue (2-5% saturation).
  4. Select semantic colors:
  5. Success: Green in the 500-600 range (e.g., #16A34A or #22C55E)
  6. Warning: Amber in the 500-600 range (e.g., #F59E0B or #EAB308)
  7. Error: Red in the 500-600 range (e.g., #EF4444 or #DC2626)
  8. Info: Blue in the 500-600 range (use primary if primary is blue, else standard blue)
  9. Generate semantic scales (at minimum: base, subtle bg, and dark-on-light text variant for each).
  10. Map to semantic tokens for light mode.
  11. Map to semantic tokens for dark mode.
  12. Test ALL combinations for WCAG AA contrast.
  13. Test with color blindness simulation.
  14. Export as design tokens (JSON, CSS custom properties, Tailwind config).

Output: The Token File

Deliver as JSON (W3C DTCG format) with both primitive and semantic layers. See brand-book-creation.md for the full token structure example.


Tailwind CSS Integration

Since GE's front-end stack uses Tailwind, every color system must export a Tailwind-compatible config.

// tailwind.config.js (client-specific)
module.exports = {
  theme: {
    extend: {
      colors: {
        // Primitives (available but discouraged in components)
        'brand-blue': {
          50: '#E8F0FE',
          100: '#D2E3FC',
          200: '#AECBFA',
          300: '#8AB4F8',
          400: '#669DF6',
          500: '#1A73E8',
          600: '#1557B0',
          700: '#104D9A',
          800: '#0D3F82',
          900: '#0A2F61',
        },
        // Semantic (USE THESE in components)
        'primary': 'var(--color-primary)',
        'primary-hover': 'var(--color-primary-hover)',
        'surface': 'var(--color-surface)',
        'text': 'var(--color-text)',
        'text-secondary': 'var(--color-text-secondary)',
        'border': 'var(--color-border)',
        'success': 'var(--color-success)',
        'warning': 'var(--color-warning)',
        'error': 'var(--color-error)',
        'info': 'var(--color-info)',
      },
    },
  },
}

Rule: Components use semantic classes (bg-primary, text-text-secondary, border-border). Primitives (bg-brand-blue-500) are only used in the theme definition CSS, never directly in components.


Checklist: Color System Delivery

  • [ ] Primitive palette generated: primary scale (10 steps), secondary (if applicable), neutrals (10 steps)
  • [ ] Semantic tokens defined for light mode (surfaces, text, borders, interactive, states)
  • [ ] Semantic tokens defined for dark mode
  • [ ] All text/background combinations tested for WCAG AA contrast (4.5:1 normal text, 3:1 large text)
  • [ ] Color blindness simulation passed (Deuteranomaly at minimum)
  • [ ] No information conveyed by color alone (icons/labels accompany all color-coded states)
  • [ ] Semantic state colors (success, warning, error, info) are standard (not rebranded)
  • [ ] Token file exported: JSON (W3C DTCG format)
  • [ ] CSS custom properties file generated
  • [ ] Tailwind config generated
  • [ ] Dark mode tested in real UI context (not just token swaps)
  • [ ] Brand color-to-UI-color mapping documented with rationale
  • [ ] If brand color conflicts with semantic meaning (red brand = error confusion), resolution documented

Anti-Patterns

Anti-Pattern Why It Fails What to Do Instead
No token layers (raw values everywhere) Cannot theme, cannot maintain, inconsistency Two-layer token model: primitive + semantic
Dark mode as afterthought Retrofitting is expensive, results in hacks Design light + dark simultaneously
Rebranding semantic colors Green "success" in brand orange confuses users Keep semantic colors universal (green/amber/red/blue)
Too many brand colors (>3) Dilutes recognition, complicates the system 1 primary + 1 secondary + neutrals
No neutral palette Designers improvise grays, resulting in 15 different grays Define a 10-step neutral scale from day one
Ignoring OKLCH HSL-generated scales look perceptually uneven Use OKLCH for perceptually uniform scales
Hardcoded colors in components Cannot theme, cannot fix globally All components reference semantic tokens only
Skipping contrast testing Accessibility failures discovered in production Test every combination during system creation
Pure black (#000000) backgrounds in dark mode Too harsh, causes halation with white text Use near-black (#121212 or darker gray)
Same brand color in dark mode Saturated colors vibrate on dark backgrounds Lighten and desaturate brand colors for dark mode

References

  • W3C Design Tokens Community Group — token format specification
  • WCAG 2.2 — Web Content Accessibility Guidelines (contrast requirements)
  • Myndex APCA — Advanced Perceptual Contrast Algorithm (emerging standard, more accurate than WCAG 2 for dark mode)
  • CSS Color Level 4 — oklch(), lch(), lab() color functions
  • Radix Colors — open-source color system designed for dark mode (excellent reference)
  • Tailwind CSS documentation — utility-first color system integration
  • Material Design 3 — Dynamic Color system (brand → UI color mapping reference)
  • Open Props — open-source CSS custom properties including color scales
  • Leonardo by Adobe — perceptual color palette generator
  • Huet — OKLCH-based color palette tool