Skip to main content

Color Palette Theory for Web Developers: Calculating Complementary, Triadic, and Analogous Schemes in HSL

How to derive complementary, triadic, and analogous color schemes programmatically using HSL — with real CSS custom property examples, hex values, and WCAG contrast checks.

Published
#color #css #web-development #design-system

Color Palette Theory for Web Developers: Calculating Complementary, Triadic, and Analogous Schemes in HSL

Color theory describes relationships using a circular coordinate system — the color wheel. HSL (hue, saturation, lightness) is the CSS format that maps directly onto that wheel. Once you see that connection, complementary, triadic, and analogous schemes reduce to arithmetic: a complementary hue is (H + 180) % 360, a triadic pair is (H + 120) % 360 and (H + 240) % 360, and an analogous neighbor is (H ± 30) % 360. This guide works through what those calculations produce, how to wire them into CSS custom properties, and where pure theory runs into WCAG contrast requirements.

HSL: The Right Coordinate System for Color Theory

CSS offers hex, RGB, HSL, HWB, LAB, LCH, and OKLCH. For color-theory arithmetic, HSL is the right starting point. Hex and RGB decompose a color into red, green, and blue channel intensities — those numbers don't correspond to anything intuitive on a color wheel. HSL uses three dimensions that do:

  • H (hue): a degree value 0–360 around the color wheel (0° = red, 120° = green, 240° = blue)
  • S (saturation): 0% = grey, 100% = fully chromatic
  • L (lightness): 0% = black, 50% = the "true" hue, 100% = white

The hue value is the bridge to color theory. Every harmony scheme is a formula over that number. Saturation and lightness stay constant across a harmonious set — four or five colors in your palette share the same energy level and the same luminance tier. You then break that constancy deliberately to make the UI readable.

Complementary Colors: One Formula, Maximum Contrast

A complementary pair sits on opposite sides of the wheel:

complementary_H = (seed_H + 180) % 360

Starting from hsl(217, 91%, 60%) — a vivid blue close to Tailwind's blue-500 (#3B82F6) — the complement hue is 217 + 180 = 397 % 360 = 37, producing hsl(37, 91%, 60%), a warm orange.

| Role | HSL | Approximate hex | |---|---|---| | Seed blue | hsl(217, 91%, 60%) | #3B82F6 | | Raw complement | hsl(37, 91%, 60%) | #F59E3B |

Running both at full saturation side by side is visually jarring — max-chroma opposites stimulate the eye from both sides simultaneously. The standard technique is to use the seed as the dominant brand color (buttons, links, key UI) and lower the complement's saturation for background use. Dropping the complement to hsl(37, 30%, 92%) creates a soft warm surface that pairs with the blue without competing.

I applied this on a side project dashboard: my seed was a mid-blue, and I used the Complementary Color Calculator to confirm the target hue, then manually tuned saturation down to 25% for the alert background. The combination read as intentional and warm without pulling attention away from the primary blue data layer.

Triadic and Analogous Schemes: The Other Intervals

Triadic distributes three colors symmetrically at 120° intervals. From seed H:

triadic_1 = (H + 120) % 360
triadic_2 = (H + 240) % 360

From blue (H = 217):

| Color | HSL | Hex | |---|---|---| | Blue (seed) | hsl(217, 91%, 60%) | #3B82F6 | | Rose (+120°) | hsl(337, 91%, 60%) | #F53B82 | | Green (+240°) | hsl(97, 91%, 60%) | #6CF53B |

Three fully saturated 60%-lightness colors vibrate against each other when used at equal weight. Color theory education, including material from the Interaction Design Foundation, consistently recommends a 60–30–10 proportion rule for multi-color systems: one dominant color covers roughly 60% of the visual surface, a secondary around 30%, and the accent is used sparingly at 10%. In practice, the triadic "accent" — whichever hue has the most punch — should appear only on the one element per screen that needs to draw the eye.

Analogous keeps all hues within a 30° arc:

analogous_prev = (H - 30 + 360) % 360
analogous_next = (H + 30) % 360

From H = 217:

| Color | HSL | Hex | |---|---|---| | Teal (−30°) | hsl(187, 91%, 60%) | #3BF5EE | | Blue (seed) | hsl(217, 91%, 60%) | #3B82F6 | | Indigo (+30°) | hsl(247, 91%, 60%) | #6B3BF5 |

Analogous schemes feel unified and calm. They work well for productivity apps, reading interfaces, and any context where a user will look at the screen for an extended stretch. Because the hues share a family, you get visual cohesion by default — the challenge is differentiating them with lightness and saturation so sections of the UI don't blur together.

Building a Full CSS Custom Property System from One Seed Hue

Here is a minimal CSS implementation that derives a working palette from a single hue variable:

:root {
  --seed-h: 217;
  --seed-s: 91%;

  /* Primary (seed) */
  --color-primary:    hsl(var(--seed-h), var(--seed-s), 55%);
  --color-primary-bg: hsl(var(--seed-h), 40%, 96%);

  /* Complement (accent / warm) */
  --comp-h: calc(var(--seed-h) + 180);
  --color-accent:     hsl(var(--comp-h), 85%, 55%);
  --color-accent-bg:  hsl(var(--comp-h), 30%, 94%);

  /* Analogous surfaces */
  --color-surface-1:  hsl(calc(var(--seed-h) - 30), 40%, 97%);
  --color-surface-2:  hsl(calc(var(--seed-h) + 30), 30%, 95%);

  /* Text (low saturation, same hue family) */
  --color-text:       hsl(var(--seed-h), 15%, 15%);
  --color-text-muted: hsl(var(--seed-h), 10%, 45%);
}

Change --seed-h from 217 to any value and the entire palette rotates consistently. This is the approach underlying design systems like Radix UI and ShadCN — a single source hue with a grid of saturation and lightness values. The advantage: rebranding from blue to teal is one variable change, and every derived surface, accent, and text color shifts in unison.

Where Theory Meets WCAG Contrast

Color harmony produces visually balanced schemes. WCAG 2.1 AA requires a minimum 4.5:1 contrast ratio for body text regardless of harmony. These two requirements sometimes conflict.

Two colors at identical saturation and lightness — the exact situation you get when you apply a color-theory formula without adjusting L — produce very low contrast. The complementary pair hsl(217, 91%, 60%) and hsl(37, 91%, 60%) has a relative luminance ratio of approximately 1.4:1. WCAG AA requires 4.5:1 for text. That gap is significant and is the most common failure pattern I see on shipped products.

The resolution is to keep the hue relationship from color theory and adjust lightness to meet contrast. A text color of hsl(217, 15%, 15%) against a background of hsl(217, 40%, 96%) produces approximately 14:1 — both colors stay in the same hue family, so they feel on-palette. Text stays dark (L ≤ 25%), backgrounds stay light (L ≥ 90%), and the color-theory hue relationship is preserved.

Before committing any palette, I run every text/background pair through the Color Contrast Checker. Paste two hex values, and it reports the contrast ratio, the WCAG level (AA / AAA, for both normal and large text), and which combinations fail. The biggest avoidable mistake in this space: approving a harmonious complementary pair in a design tool at 100% zoom without checking the numbers, then discovering the combination is inaccessible after it ships to production.

A reliable shortcut: any foreground color at L ≤ 25% on a white (L = 100%) background clears WCAG AA with headroom. Any foreground at L ≥ 80% on white will almost certainly fail — no matter how harmonious the palette looks on the color wheel.

Putting It Together

The practical workflow is:

  1. Choose one seed hue based on brand or mood.
  2. Derive complement, triadic, and analogous hues using the arithmetic above.
  3. Assign each hue a role (primary, background, accent, text) with appropriate saturation and lightness for that role.
  4. Verify every foreground/background pair for WCAG contrast before shipping.

Color theory gives you hue relationships that feel coherent and intentional. Lightness and saturation adjustments make those relationships usable in real interfaces. Neither step replaces the other — you need both.


Made by Toolora · Updated 2026-06-30