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.