Color Theory Foundations & Harmony
Master perceptual color principles, harmony systems, and modern OKLCH workflows to build palettes that communicate, scale, and hold up under real constraints.
10 min read
The full lesson
Color does far more than decoration. The moment it hits a screen, it directs attention, signals meaning, creates hierarchy, and communicates brand — all before a user reads a single word. Getting it right means understanding how humans actually perceive color, not just how color wheels look in design tools.
This lesson builds that foundation. We start with how the eye works, move through harmony systems designers use every day, and finish with the modern OKLCH-first workflow that has replaced hand-picked HSL palettes in serious design systems.
How Human Color Perception Works
Your eye has three types of cone cells. Each is most sensitive to a different range of wavelengths: long (red), medium (green), and short (blue). Your brain combines their signals into three perceptual channels: lightness, red-green opposition, and blue-yellow opposition. This is called the opponent-color model, and it explains several phenomena that matter directly to your design decisions.
Simultaneous contrast means the same neutral gray looks lighter on a dark background and darker on a light one. This is not an optical illusion — it is accurate perception of relative luminance. Every color you place reads differently depending on what surrounds it. A vivid mid-blue on white will look flat on dark navy.
Chromatic adaptation means your visual system constantly recalibrates its sense of “white” based on the light source around you. Screens emit light rather than reflecting it, so a balanced palette on one monitor can read as warm or cold on another. Always evaluate palettes across device profiles, not just in a single Figma window.
Color induction means a saturated color pushes nearby neutrals toward its complementary hue. Place a vivid red next to a gray swatch and the gray will subtly read as greenish. This matters when you use neutral backgrounds in branded interfaces.
Color Models and Why the Space Matters
A color model is a coordinate system for describing color. Different models are built for different jobs. Choosing the wrong one for authoring creates real production problems.
| Model | What it encodes | Primary use case | Key limitation |
|---|---|---|---|
| sRGB / Hex | Red, Green, Blue additive primaries | Screen rendering output | Non-perceptual; equal numeric steps produce unequal visual steps |
| HSL | Hue, Saturation, Lightness | Fast picker UX | Lightness channel is not perceptually uniform — yellow and blue at L=50% look wildly different |
| HSB / HSV | Hue, Saturation, Brightness | Legacy design tool pickers | Same non-uniformity problem as HSL |
| OKLCH | Lightness, Chroma, Hue (perceptual) | Modern palette authoring | Requires CSS Color Level 4; widely supported since 2023 |
| Display-P3 | Wide-gamut RGB | Modern displays and OS | Colors outside sRGB gamut are invisible on older hardware |
The core problem with HSL is that equal lightness values do not produce equal visual brightness. A yellow at hsl(60, 100%, 50%) appears dramatically brighter than a blue at hsl(240, 100%, 50%). That makes it impossible to build a tonal scale in HSL with consistent perceptual contrast between steps. Every value needs hand-correction, and even experienced designers get this wrong.
OKLCH solves this. It is a perceptually uniform color space derived from the Oklab model, introduced by Björn Ottosson in 2020 and built on CIECAM02 research. In OKLCH, moving the Lightness coordinate by a fixed amount produces a visually equal change in perceived brightness — no matter what hue you are on. That makes algorithmic tonal scale generation reliable: set a target lightness step, iterate, and get a consistent palette.
The three coordinates are:
- L — perceived lightness, from 0 (black) to 1 (white)
- C — chroma (colorfulness), from 0 (achromatic gray) to roughly 0.4 at the sRGB gamut boundary, and higher in wide-gamut spaces
- H — hue angle in degrees from 0 to 360, with red near 30°, yellow near 100°, green near 145°, and blue near 250°
Classic Harmony Systems
Harmony describes hue relationships that human perception finds coherent. These systems are built around the hue axis of a perceptual color wheel. Think of them as tools for generating candidate palettes — not formulas that guarantee a good design on their own.
Complementary
Complementary colors sit roughly 180 degrees apart on the hue wheel. They create maximum contrast and high visual tension. This makes them excellent for call-to-action accents against a primary brand color. At equal saturation across large areas they become fatiguing — the classic fix is to reduce chroma or shift lightness on one of the pair. In OKLCH: if your brand is at H=250 (blue), your complement lands near H=70 (yellow-orange).
Analogous
Analogous colors are three to five adjacent hues spanning roughly 30–90 degrees. They are low contrast and high cohesion — natural and calm. This works well for backgrounds and illustration. Watch out: adjacent UI elements need deliberate value contrast between them to stay legible.
Triadic
A triadic scheme uses three hues equally spaced 120 degrees apart. The result is vibrant and energetic, but harder to balance. In practice one hue dominates (roughly 60%), one supports (30%), and one accents (10%). This 60-30-10 proportion principle applies across all multi-hue schemes.
Split-Complementary
This takes a base hue and adds the two hues adjacent to its complement. You get most of the contrast of a complementary pair, but with less visual harshness. It is more forgiving than a strict complementary pair and a good starting point for product UI color systems.
Tetradic / Square
Four hues arranged in two complementary pairs. Rich potential, but genuinely difficult to execute. Almost always requires strict value and saturation discipline, with one hue clearly dominant, to avoid visual chaos.
Building a Perceptually Consistent Tonal Scale
A tonal scale is a sequence of lightness steps across a single hue — typically 9–12 steps — used as raw material for a design system palette. Modern practice generates these algorithmically in OKLCH rather than hand-picking hex values.
A minimal OKLCH tonal scale approach:
- Choose a base hue (H) and a maximum chroma (C) appropriate for that hue. Yellow-green hues tolerate higher chroma at middle lightness. Blue and purple hues clip into the P3 gamut at high chroma and need lower C values.
- Define lightness stops. A common convention: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 — where 50 is near-white (L ≈ 0.97) and 950 is near-black (L ≈ 0.10).
- For each stop, specify
oklch(L C H)with L varying linearly, or with a slight ease at the extremes. Taper chroma at the lightest and darkest ends to avoid washed-out pastels or muddy darks. - Check each adjacent pair for sufficient contrast. A well-constructed 11-step scale typically achieves WCAG 2.2 AA (4.5:1 contrast ratio) at 3–4 steps of separation, giving you a predictable formula for text pairings.
The output — blue-50 through blue-950 — is a set of primitive tokens. They are not referenced directly in component CSS; they feed the semantic layer.
Saturation, Chroma, and Visual Weight
Saturation in HSL is relative: it describes how much of the maximum colorfulness is present at a given lightness. Chroma in OKLCH is absolute: it measures actual perceived colorfulness, independent of lightness. Chroma is the more useful concept because it predicts how vivid a color will actually appear.
Practical distribution:
- High chroma — use it for small accents, interactive focus indicators, status badges, and data visualization categories. It directs attention effectively but loses its power when overused.
- Medium chroma — ideal for brand colors at middle lightness. Visible and distinct without being fatiguing. This is the core of most product identities.
- Low chroma (near-neutral) — best for backgrounds, surfaces, and large text containers. Saturated backgrounds compete with foreground content and increase cognitive load.
- Chroma tapering — reduce chroma as lightness approaches either extreme. A fully saturated near-white looks synthetic and harsh. A fully saturated near-black is indistinguishable from pure black in most viewing conditions.
Do
Use high chroma sparingly for interactive elements, alerts, and data categories. Pair vivid accents against low-chroma surfaces so the accent carries its full visual weight. Build tonal scales where chroma decreases toward the light and dark ends. Generate scales algorithmically in OKLCH to ensure perceptual consistency.
Don't
Apply high saturation to large background areas or body text regions. Use the same chroma value across all steps of a tonal scale. Mix color spaces within a single palette — some swatches in HSL, some in OKLCH — the perceptual inconsistency will surface as uneven contrast in the final UI. Use Sass darken() and lighten() to derive tonal variants from a single hex value; the results are not perceptually consistent.
Color Meaning, Semantics, and Cultural Context
Color carries meaning, but that meaning varies by culture, domain, and context. In digital UI these are learned conventions, not universal truths.
Common conventions in Western digital interfaces:
- Red — error, danger, destructive action, negative financial delta
- Green — success, confirmation, positive financial delta, go state
- Yellow / amber — warning, caution, pending or in-progress state
- Blue — information, primary interactive color, default link color in most systems
- Gray — disabled state, secondary or tertiary information hierarchy
Where these break down:
- Color vision deficiency — protanopia and deuteranopia (red-green variants) affect roughly 8% of males and 0.5% of females. Never use red-versus-green as the only differentiator between states. Always pair color with a second signal: icon shape, label text, pattern fill, or positional encoding.
- Cultural variation — red represents prosperity in Chinese contexts. In some West African traditions, white carries mourning associations. Financial apps face a direct conflict: in the United States, red conventionally signals financial loss, while in China it signals gain. Global products need explicit user preferences, not hardcoded color mappings.
- Domain overrides convention — medical interfaces sometimes invert the warning hierarchy, using amber for moderate findings and red only for critical ones. Always validate semantic color choices with domain experts and real users, not just general UI conventions.
Contrast and the Accessibility Baseline
Luminance contrast is the most critical measurable property of any color pairing. WCAG 2.2 defines it as the ratio (L1 + 0.05) / (L2 + 0.05), where L1 is the lighter relative luminance and L2 is the darker. The scale runs from 1:1 (no contrast) to 21:1 (black on white).
WCAG 2.2 AA thresholds (current legal baseline):
- 4.5:1 — normal-weight text below 18pt (or 14pt bold)
- 3:1 — large text (18pt+ regular, 14pt+ bold) and non-text UI components (input borders, focus rings, icon-only buttons)
- Focus indicators specifically require 3:1 contrast against adjacent colors (SC 2.4.11, added in WCAG 2.2)
APCA (Advanced Perceptual Contrast Algorithm) is a more sophisticated model that accounts for polarity (light-on-dark vs. dark-on-light), font weight, and point size. It produces more accurate legibility predictions than the WCAG 2.1 formula, which uses a simplified luminance calculation that over-penalizes some mid-tone pairings and under-penalizes others at very high contrast. However, APCA has not been adopted in any published WCAG version. Treat it as a supplementary perceptual-quality lens while keeping WCAG 2.2 AA as your compliance target.
Modern design workflows integrate contrast checking directly. Figma and Penpot display live ratios, and token systems can run automated checks in CI via axe-core or pa11y to catch violations before code ships.
From Primitive Colors to Semantic Tokens
The tonal scales you build are primitive tokens — they describe color values with no implied meaning. Well-structured design systems never reference primitives directly in component styles. Instead they use semantic tokens that name purpose.
Three-tier architecture (W3C Design Token Community Group format):
{
"color": {
"primitive": {
"blue-500": { "$value": "oklch(0.55 0.18 250)", "$type": "color" }
},
"semantic": {
"interactive-default": { "$value": "{color.primitive.blue-500}", "$type": "color" }
},
"component": {
"button-background": { "$value": "{color.semantic.interactive-default}", "$type": "color" }
}
}
}
A theme change, dark mode toggle, or brand refresh only requires updating the semantic layer. Components reference button-background, not oklch(0.55 0.18 250), so a single semantic reassignment propagates across the entire system.
The outdated practice of naming semantic tokens after their primitive value — using color-blue-500 as both the primitive name and the semantic alias — destroys this abstraction entirely. Semantic token names must describe role and context, not color value.