wcagColors.js is a dependency-free ESM utility for generating, filtering, sorting, and converting colors with WCAG-focused contrast workflows.
It is designed for fast color candidate exploration in apps, design tooling, and accessibility checks.
- Pure functions (no internal mutable singleton state)
- Strict color parsing with clear errors for unsupported formats
- End-to-end color pipelines: generate -> filter -> sort -> output
- WCAG relative luminance and contrast helpers
- Lightweight conversion helpers (RGB, HSL, HEX)
- JavaScript runtime with ESM support
- Node.js 18+ recommended
This repository ships a single module entrypoint.
import {
parseColor,
generateColors,
filterColors,
sortColors,
toHexArray
} from "./wcagColors.js";import {
generateColors,
filterColors,
sortColors,
toHexArray
} from "./wcagColors.js";
const pool = generateColors({
color: "#0000ff",
minHueDiff: 0,
maxHueDiff: 10,
stepHue: 5,
minSaturation: 0.4,
maxSaturation: 1,
stepSaturation: 0.05,
minLightness: 0,
maxLightness: 1,
stepLightness: 0.05
});
const readableOnWhite = filterColors(pool, {
color: "#ffffff",
minContrast: 7
});
const readableOnWhiteAndText = filterColors(readableOnWhite, {
color: "#000000",
minContrast: 3
});
const result = toHexArray(sortColors(readableOnWhiteAndText, "s", "asc"));
console.log(result);Most APIs work with this normalized color object:
{
r: 51,
g: 102,
b: 153,
h: 210,
s: 0.5,
l: 0.4,
hex: "#336699",
lum: 0.1250645743,
contrast: 4.93 // optional, added by filterColors when contrast filtering is used
}Parses and normalizes color input into a full color object.
Supported input formats:
- HEX strings:
#rgb,#rrggbb - RGB strings:
rgb(r,g,b),rgba(r,g,b,a) - HSL strings:
hsl(h,s%,l%),hsla(h,s%,l%,a) - RGB objects:
{ r, g, b } - HSL objects:
{ h, s, l } - Objects with a
hexproperty
import { parseColor } from "./wcagColors.js";
parseColor("#fff");
parseColor("rgb(51, 102, 153)");
parseColor("hsl(210, 50%, 40%)");
parseColor({ r: 51, g: 102, b: 153 });
parseColor({ h: 210, s: 0.5, l: 0.4 });Throws:
Errorif input type or format is unsupported
Generates deduplicated candidate colors from hue, saturation, and lightness ranges.
Parameters:
color(string | object, optional): base color used to infer hue ifhueis not providedhue(number, optional): base hue in degreesminSaturation(number, default0)maxSaturation(number, default1)stepSaturation(number, default0.1, must be> 0)minLightness(number, default0)maxLightness(number, default1)stepLightness(number, default0.1, must be> 0)minHueDiff(number, default0)maxHueDiff(number, default360)stepHue(number, default15, must be> 0)
import { generateColors } from "./wcagColors.js";
const colors = generateColors({
hue: 220,
minHueDiff: 0,
maxHueDiff: 20,
stepHue: 5,
minSaturation: 0.2,
maxSaturation: 0.9,
stepSaturation: 0.05,
minLightness: 0.2,
maxLightness: 0.8,
stepLightness: 0.05
});Returns:
ParsedColor[]
Throws:
Errorif neitherhuenor validcoloris providedErrorif a step value is zero or negative
Filters candidate colors by minimum contrast and/or hue difference from a reference color.
Parameters:
colors(ParsedColor[]): source candidatescriteria.color(string | object, required when using hue or contrast filters)criteria.minContrast(number, optional)criteria.minHueDiff(number, optional)criteria.maxHueDiff(number, optional)
import { filterColors } from "./wcagColors.js";
const filtered = filterColors(colors, {
color: "#ffffff",
minContrast: 4.5,
minHueDiff: 10,
maxHueDiff: 60
});Notes:
- If no filtering keys are provided, it returns a shallow copy of the input array.
- If
minContrastis used, each returned item includes a computedcontrastvalue. - Hue difference is circular (for example, 5 degrees and 355 degrees are 10 degrees apart).
Sorts colors without mutating the input array.
Parameters:
colors(ParsedColor[])sortBy(string): common keys includer,g,b,h,s,l,lum,contrast,hexdirection("asc" | "desc", default"desc")
import { sortColors } from "./wcagColors.js";
const byContrast = sortColors(filtered, "contrast", "desc");
const bySaturation = sortColors(filtered, "s", "asc");Maps normalized color objects to a plain hex string array.
import { toHexArray } from "./wcagColors.js";
const hexes = toHexArray(filtered);- Converts RGB channels to normalized 6-digit hex
- Converts
#rgbor#rrggbbto{ r, g, b }
- Converts HSL to RGB
- Converts RGB to HSL
- Computes WCAG relative luminance
- Computes WCAG contrast ratio between two luminance values
import { generateColors, filterColors, sortColors } from "./wcagColors.js";
const base = "#f4f1ea";
const candidates = generateColors({
color: base,
minHueDiff: 120,
maxHueDiff: 240,
stepHue: 10,
minSaturation: 0,
maxSaturation: 0.4,
stepSaturation: 0.05,
minLightness: 0,
maxLightness: 0.7,
stepLightness: 0.05
});
const readable = filterColors(candidates, { color: base, minContrast: 4.5 });
const best = sortColors(readable, "contrast", "desc")[0];import { generateColors, filterColors, sortColors, toHexArray } from "./wcagColors.js";
const background = "#ffffff";
const text = "#222222";
const pool = generateColors({
hue: 210,
minHueDiff: 0,
maxHueDiff: 20,
stepHue: 2,
minSaturation: 0.3,
maxSaturation: 1,
stepSaturation: 0.02,
minLightness: 0,
maxLightness: 0.9,
stepLightness: 0.02
});
const onBackground = filterColors(pool, { color: background, minContrast: 4.5 });
const distinctFromText = filterColors(onBackground, { color: text, minContrast: 3 });
const ranked = sortColors(distinctFromText, "contrast", "desc");
const hexes = toHexArray(ranked);- Wrap user-provided color parsing in
try/catch. - Validate user sliders or form input before calling
generateColors. - Keep step values greater than zero.
import { parseColor } from "./wcagColors.js";
try {
const color = parseColor(userInput);
console.log(color);
} catch (error) {
console.error("Invalid color input", error.message);
}- Smaller steps produce larger candidate pools.
- Start with coarse steps (
0.1,10) and refine only where needed. - Apply restrictive filters early to reduce sort cost.
- Use benchmarks in this repo to tune your workload.
Run local checks:
npm test
npm run benchMIT