DEV Community

Michael Lip
Michael Lip

Posted on • Originally published at zovo.one

Color Theory for Developers: Why Complementary Colors Work and How to Compute Them

I picked colors for a side project by intuition. Blue for the header, green for buttons, orange for accents. It looked terrible. Not because the individual colors were bad, but because the combination had no logic. The blue and green were too close in hue, creating a muddy, indecisive feeling. The orange clashed with both because it was not positioned correctly relative to either color on the spectrum.

Then I learned about color wheel relationships, and choosing colors stopped being a guessing game.

The color wheel is a coordinate system

The color wheel arranges hues in a circle based on their wavelength relationships. The standard wheel places primary colors (red, yellow, blue in traditional theory; red, green, blue in light-based theory) at equal intervals, with secondary and tertiary colors filling the gaps.

In HSL terms, hue is a 360-degree angle on this wheel:

0   / 360: Red
30:        Orange
60:        Yellow
120:       Green
180:       Cyan
240:       Blue
270:       Purple
300:       Magenta
Enter fullscreen mode Exit fullscreen mode

Every color harmony (complementary, analogous, triadic, etc.) is a geometric relationship on this wheel -- specific angles between selected hues.

The five fundamental harmonies

Complementary (180 degrees apart). Two colors on opposite sides of the wheel. They create maximum contrast and visual tension. Blue (#0066CC, hue 210) and orange (#CC6600, hue 30). The 180-degree separation means each color contains none of the other's hue component, making them pop against each other.

--primary: hsl(210, 100%, 40%);
--accent: hsl(30, 100%, 40%);     /* 210 + 180 = 390 = 30 */
Enter fullscreen mode Exit fullscreen mode

Use complementary colors for call-to-action elements against a dominant background. The risk is that equal amounts of two complementary colors create visual vibration (a buzzing effect at their boundary). Use one as the dominant color (60-70% of the design) and the other as an accent (10-20%).

Analogous (30 degrees apart). Three colors adjacent on the wheel. They create a harmonious, low-contrast palette. Blue (210), blue-green (180), blue-violet (240). These palettes feel cohesive and calm because the hues share dominant wavelengths.

--primary: hsl(210, 70%, 45%);
--secondary: hsl(180, 70%, 45%);   /* 210 - 30 */
--tertiary: hsl(240, 70%, 45%);    /* 210 + 30 */
Enter fullscreen mode Exit fullscreen mode

The risk: too little contrast. Analogous palettes need variation in lightness and saturation to prevent the design from looking flat. Make one color dark and saturated, one medium, and one light or desaturated.

Triadic (120 degrees apart). Three colors evenly spaced around the wheel. Red (0), green (120), blue (240). Triadic palettes are vibrant and balanced. They provide strong visual contrast while maintaining harmony because of the equal spacing.

--primary: hsl(0, 70%, 50%);
--secondary: hsl(120, 70%, 50%);
--tertiary: hsl(240, 70%, 50%);
Enter fullscreen mode Exit fullscreen mode

Triadic is the hardest harmony to execute well. All three colors compete for attention. The standard approach is 60-30-10: 60% of the design uses one color, 30% uses the second, and 10% uses the third.

Split-complementary (150 and 210 degrees). Instead of the direct complement, use the two colors flanking it. If your base color is blue (210), the complement is orange (30), and the split-complement uses yellow-orange (60) and red-orange (0). This provides similar contrast to complementary but with less tension.

--primary: hsl(210, 70%, 45%);
--accent-1: hsl(60, 70%, 45%);    /* complement 30, split +30 */
--accent-2: hsl(0, 70%, 45%);     /* complement 30, split -30 */
Enter fullscreen mode Exit fullscreen mode

Tetradic / Square (90 degrees apart). Four colors at 90-degree intervals. This gives the richest palette but is the hardest to balance. It works best in complex designs like dashboards or magazine layouts where you need many distinct color categories.

Computing harmonies programmatically

Since harmonies are just angle arithmetic, generating them is straightforward:

function getHarmonies(baseHue) {
  return {
    complementary: [(baseHue + 180) % 360],
    analogous: [
      (baseHue - 30 + 360) % 360,
      (baseHue + 30) % 360
    ],
    triadic: [
      (baseHue + 120) % 360,
      (baseHue + 240) % 360
    ],
    splitComplementary: [
      (baseHue + 150) % 360,
      (baseHue + 210) % 360
    ],
    tetradic: [
      (baseHue + 90) % 360,
      (baseHue + 180) % 360,
      (baseHue + 270) % 360
    ]
  };
}

// Example: harmonies for blue (hue 210)
getHarmonies(210);
// complementary: [30]           (orange)
// analogous: [180, 240]         (cyan, blue-violet)
// triadic: [330, 90]            (pink, yellow-green)
// splitComplementary: [0, 60]   (red, yellow)
// tetradic: [300, 30, 120]      (magenta, orange, green)
Enter fullscreen mode Exit fullscreen mode

But hue alone does not make a usable palette. You need to vary saturation and lightness for each role:

  • Background colors: low saturation, high lightness (or low lightness for dark mode)
  • Primary/brand color: high saturation, medium lightness
  • Text colors: very low saturation, very low lightness (or very high for dark mode)
  • Accent/CTA colors: high saturation, medium lightness, complementary or triadic hue

Common mistakes

Using fully saturated colors everywhere. Saturation 100% for all colors creates a garish, overwhelming palette. In practice, backgrounds should be 5-15% saturation, body text 10-20%, and only accent elements should approach full saturation.

Ignoring the perceptual non-uniformity of HSL. As I discussed in my color converter article, HSL lightness is not perceptually uniform. Yellow at lightness 50% looks much brighter than blue at lightness 50%. If you are generating a palette where all colors need equal visual weight, use OKLCH instead of HSL for your lightness values.

Picking colors in isolation. A color that looks perfect on a white artboard may not work in context. Always test your palette against the actual content: text, images, icons, and interactive elements. The color wheel gives you a starting point, not a finished palette.

Forgetting about neutral colors. A palette of five vivid hues does not make a design. You need neutral grays (or slightly tinted grays) for backgrounds, borders, disabled states, and secondary text. Derive your neutrals from your primary hue: hsl(210, 10%, 95%) for a light background that subtly echoes a blue primary.

When I am starting a new design and need to explore color relationships, I use the color wheel at zovo.one/free-tools/color-wheel to generate harmonies and see how they interact visually.

The color wheel does not replace design intuition. But it gives intuition a framework to work within. Constraints breed creativity, and "choose colors that are 120 degrees apart" is a much better constraint than "choose colors that look nice."


I'm Michael Lip. I build free developer tools at zovo.one. 350+ tools, all private, all free.

Top comments (0)