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¶
- HSL manipulation (recommended starting point):
- Keep Hue constant (or shift by max 5 degrees across the scale).
- Decrease Saturation as you go lighter (tints should not be neon).
- Increase Lightness for tints, decrease for shades.
-
The 50 step should be ~95-97% lightness. The 900 step should be ~15-20% lightness.
-
Perceptual uniformity (advanced, recommended for production):
- HSL is not perceptually uniform — a 10% lightness change in yellow looks different from 10% in blue.
- Use OKLCH or CIELAB color spaces for perceptually even steps.
- OKLCH:
oklch(L C H)— L is perceptual lightness (0–1), C is chroma, H is hue. -
Modern CSS supports
oklch()natively. This is the future-proof approach. -
Bezier curve interpolation:
- Plot the anchor color on a curve from white to black.
- Interpolate intermediate steps along the curve.
- Tools: Huet, Leonardo (Adobe), Scale by Colorbox.
Neutral Palette¶
Every system needs a neutral (gray) scale. Two approaches:
- Pure gray: Neutral without hue. Clean, safe, works with any brand color.
- Warm/cool gray (recommended): Add a tiny amount of the brand hue to the gray scale. This makes neutrals feel cohesive with the brand.
- Warm: Add 2–5% saturation with a warm hue (30–50 degrees).
- Cool: Add 2–5% saturation with a cool hue (210–230 degrees).
- 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¶
-
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.
-
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)
- 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
- 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¶
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¶
- Generate primary scale (10 steps) using OKLCH or HSL manipulation.
- Generate secondary scale (10 steps) if a secondary brand color exists.
- Generate neutral scale (10 steps) tinted with the primary hue (2-5% saturation).
- Select semantic colors:
- Success: Green in the 500-600 range (e.g.,
#16A34Aor#22C55E) - Warning: Amber in the 500-600 range (e.g.,
#F59E0Bor#EAB308) - Error: Red in the 500-600 range (e.g.,
#EF4444or#DC2626) - Info: Blue in the 500-600 range (use primary if primary is blue, else standard blue)
- Generate semantic scales (at minimum: base, subtle bg, and dark-on-light text variant for each).
- Map to semantic tokens for light mode.
- Map to semantic tokens for dark mode.
- Test ALL combinations for WCAG AA contrast.
- Test with color blindness simulation.
- 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