Color Theory in Code: How Our Palette Generator Builds Harmonies and Checks Contrast

Color theory articles usually show color wheels and name feelings. This one shows the

arithmetic. Our palette generator implements each classical harmony as a fixed rotation

in HSL space, and our contrast checker implements WCAG luminance with gamma correction.

Reading both reveals a trap most guides skip: the two tools use two different

definitions of brightness, and a palette that looks balanced can still fail

accessibility.

Every harmony is a rotation plus unchanged S and L

The generator converts your base color from hex to HSL, then produces companions by

shifting hue while copying saturation and lightness exactly. The offsets, straight from

the code:

  • Complementary: base plus 180 degrees, a two-color palette.
  • Triadic: base plus 120 and plus 240, a three-color palette. This is the default.
  • Analogous: minus 30, base, plus 30, three neighbors on the wheel.
  • Split complementary: base plus 150 and plus 210.
  • Tetradic: base plus 90, plus 180, plus 270, four colors.
  • Monochromatic: same hue and saturation, lightness shifted to L plus 30 and L minus 30.

The monochromatic branch is the only one that touches lightness, and it clamps: the

lighter step never exceeds 90, the darker step never drops below 10. That clamp keeps

both steps inside the displayable range instead of producing white or black.

The default base color is 6368f1, a medium indigo, and the default type is triadic. You

can type any six-digit hex value, but the input only accepts values matching a strict

six-character hex pattern, so three-digit shorthand like fff is rejected until

expanded.

Why equal lightness creates equal-looking, unusable palettes

Copying lightness across every harmony color is what makes generated palettes feel

coherent. It also means no pair in the palette is guaranteed to differ enough in

luminance for text. A yellow at 60 percent HSL lightness and a blue at 60 percent HSL

lightness carry very different perceived brightness, because HSL lightness is a

geometric midpoint of RGB, and human vision weights green far above blue.

This is the structural weakness of wheel-based harmony. It controls one dimension of a

three-dimensional quantity. Use harmonies to pick candidate hues, then adjust lightness

per color rather than trusting the generated values for foreground and background

pairs.

The two brightness formulas living on the same site

Here is the detail that most color guides never mention. Our palette swatches pick

their label text color using the classic SDTV luma weights:

0.299 * r + 0.587 * g + 0.114 * b

If that value exceeds 0.5, the label goes dark slate, otherwise white. Our contrast

checker uses the WCAG formula instead, which applies different weights plus a gamma

curve on each channel before combining:

0.2126 * R + 0.7152 * G + 0.0722 * B

Both formulas are correct for their own job. Luma is fast and good enough for choosing

one of two label colors on a saturated block. WCAG luminance is the definition the

accessibility rules are written against. The consequence for you: a swatch label that

reads fine is zero evidence that the same color pair passes any WCAG threshold. Test

text pairs with the checker, never with your eye or the swatch preview.

The contrast numbers that actually govern accessibility

The checker computes the ratio as lighter luminance plus 0.05, divided by darker

luminance plus 0.05, and compares against four thresholds:

1. 4.5:1 for normal text at WCAG AA.

2. 3:1 for large text at WCAG AA.

3. 7:1 for normal text at WCAG AAA.

4. 4.5:1 for large text at WCAG AAA.

Two failure cases follow from the math. First, pure white on pure black is the maximum

at 21:1, so every real pair sits below it, and pushing a gray background from 90

percent to 95 percent lightness costs you a large chunk of ratio. Second, the 3:1

large-text rule tempts people to enlarge failing text instead of fixing colors. Large

means at least 18.66 pixel bold or 24 pixel regular. Enlarging body copy to pass is

usually a design failure, so fix the pair.

A practical workflow, in order:

1. Pick a base color and a harmony type, and export the candidates.

2. Choose one color as the surface and one as text.

3. Run the pair through the contrast checker against 4.5:1.

4. If it fails, change the text color lightness only, keeping the hue. Recheck.

5. Copy the final hex values, and export the palette as CSS variables.

The CSS export names them color-1 through color-N, so rename them by role, such as

surface and accent, before shipping. Role names survive palette changes. Numbered

names do not.

Checklist before a palette ships

  • Harmony selected by hue rotation, then lightness adjusted per color by hand.
  • Every text and background pair checked at 4.5:1 minimum for body text.
  • Large text claimed at 3:1 only where text is genuinely large.
  • Hue decisions from the palette tool, luminance decisions from the contrast tool.
  • CSS variables renamed from numbered keys to role names.

Wheel harmonies give you a defensible starting set in seconds. Contrast math decides

whether the set can carry text. Keep those two decisions separate and your palettes

will look intentional and pass review.

Which pair surprised you by failing, a light-on-light combo or something you expected

to be safe? Build the candidate set with our

[color palette generator](https://webrecast.com/en/color-palette) and run the pairs

through the checker before you commit.