Skip to content

Recipes

Quick snippets for common color tasks.

import { contrast } from 'colorizr';
const ratio = contrast('#1a1a2e', '#ffffff'); // 17.05
// WCAG AA requires 4.5:1 for normal text, 3:1 for large text
// WCAG AAA requires 7:1 for normal text, 4.5:1 for large text

See contrast and compare for full WCAG analysis.

import { readableColor } from 'colorizr';
readableColor('#1a1a2e'); // '#ffffff'
readableColor('#f0f0f0'); // '#000000'
readableColor('#663399', 'apca'); // '#ffffff' (using APCA algorithm)

See readableColor for all methods and options.

import { scale } from 'colorizr';
const blue = scale('#3366ff');
// { 50: '#f0f5ff', 100: '#e4edff', ..., 900: '#06009a', 950: '#020052' }
const muted = scale('#3366ff', { variant: 'neutral' });
const pastel = scale('#3366ff', { variant: 'pastel' });

See scale for variants, step counts, and curve options.

import { scheme } from 'colorizr';
scheme('#ff0044'); // complementary (default)
scheme('#ff0044', 'analogous'); // 3 adjacent colors
scheme('#ff0044', 'triadic'); // 3 evenly spaced colors
scheme('#ff0044', 'tetradic'); // 4 colors (rectangle)

See scheme for all harmony types.

import { convertCSS } from 'colorizr';
convertCSS('#ff0044', 'hsl'); // 'hsl(344 100% 50%)'
convertCSS('#ff0044', 'oklch'); // 'oklch(63.269% 0.25404 19.902)'
convertCSS('#ff0044', 'rgb'); // 'rgb(255 0 68)'

See converters for all format conversions.

import { mix } from 'colorizr';
mix('#ff0044', '#0066ff'); // 50/50 mix in OkLCH
mix('#ff0044', '#0066ff', 0.2); // 80% first, 20% second
mix('#ff0044', '#0066ff', 0.5, { space: 'hsl' }); // mix in HSL space

See mix for interpolation spaces and hue modes.

Color pickers traditionally use HSV (Hue, Saturation, Value) for their 2D panel. To support wide-gamut P3 colors with OkLCH, you can use colorizr’s exported constants and helpers to build the conversion pipeline.

HSV isn’t a CSS color format — it’s a UI representation. You’ll need two simple functions for HSV ↔ RGB conversion:

function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
const c = v * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = v - c;
let r = 0, g = 0, b = 0;
if (h < 60) { r = c; g = x; }
else if (h < 120) { r = x; g = c; }
else if (h < 180) { g = c; b = x; }
else if (h < 240) { g = x; b = c; }
else if (h < 300) { r = x; b = c; }
else { r = c; b = x; }
return [r + m, g + m, b + m];
}
function rgbToHsv(r: number, g: number, b: number): { h: number; s: number; v: number } {
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const delta = max - min;
let h = 0;
if (delta !== 0) {
if (max === r) h = ((g - b) / delta) % 6;
else if (max === g) h = (b - r) / delta + 2;
else h = (r - g) / delta + 4;
h *= 60;
if (h < 0) h += 360;
}
return { h, s: max === 0 ? 0 : delta / max, v: max };
}

Convert an OkLCH color to P3 HSV for rendering on a wide-gamut canvas. This bypasses sRGB clamping so P3 colors display correctly:

import {
OKLAB_TO_CLMS, LMS_TO_LRGB, SRGB_TO_P3,
DEG2RAD, srgbGammaEncode,
} from 'colorizr';
function oklchToP3Hsv(l: number, c: number, h: number) {
// OkLCH → OkLab
const hRad = h * DEG2RAD;
const labA = c * Math.cos(hRad);
const labB = c * Math.sin(hRad);
// OkLab → LMS cube roots → LMS
const cl = OKLAB_TO_CLMS[0][0] * l + OKLAB_TO_CLMS[0][1] * labA + OKLAB_TO_CLMS[0][2] * labB;
const cm = OKLAB_TO_CLMS[1][0] * l + OKLAB_TO_CLMS[1][1] * labA + OKLAB_TO_CLMS[1][2] * labB;
const cs = OKLAB_TO_CLMS[2][0] * l + OKLAB_TO_CLMS[2][1] * labA + OKLAB_TO_CLMS[2][2] * labB;
const lms_l = cl ** 3;
const lms_m = cm ** 3;
const lms_s = cs ** 3;
// LMS → sRGB linear
const sr = LMS_TO_LRGB[0][0] * lms_l + LMS_TO_LRGB[0][1] * lms_m + LMS_TO_LRGB[0][2] * lms_s;
const sg = LMS_TO_LRGB[1][0] * lms_l + LMS_TO_LRGB[1][1] * lms_m + LMS_TO_LRGB[1][2] * lms_s;
const sb = LMS_TO_LRGB[2][0] * lms_l + LMS_TO_LRGB[2][1] * lms_m + LMS_TO_LRGB[2][2] * lms_s;
// sRGB linear → P3 linear → P3 gamma
const p3r = SRGB_TO_P3[0][0] * sr + SRGB_TO_P3[0][1] * sg + SRGB_TO_P3[0][2] * sb;
const p3g = SRGB_TO_P3[1][0] * sr + SRGB_TO_P3[1][1] * sg + SRGB_TO_P3[1][2] * sb;
const p3b = SRGB_TO_P3[2][0] * sr + SRGB_TO_P3[2][1] * sg + SRGB_TO_P3[2][2] * sb;
const r = Math.max(0, Math.min(1, srgbGammaEncode(p3r)));
const g = Math.max(0, Math.min(1, srgbGammaEncode(p3g)));
const b = Math.max(0, Math.min(1, srgbGammaEncode(p3b)));
return rgbToHsv(r, g, b);
}

When rendering the picker panel, you can visualize which colors fall outside sRGB by checking each P3 HSV color against the sRGB gamut:

import { P3_TO_XYZ, XYZ_TO_SRGB, GAMUT_EPSILON, srgbGammaDecode } from 'colorizr';
function isP3HsvInSRGB(h: number, s: number, v: number): boolean {
const [r, g, b] = hsvToRgb(h, s, v);
// P3 gamma → P3 linear
const rLin = srgbGammaDecode(r);
const gLin = srgbGammaDecode(g);
const bLin = srgbGammaDecode(b);
// P3 linear → XYZ D65 → sRGB linear
const x = P3_TO_XYZ[0][0] * rLin + P3_TO_XYZ[0][1] * gLin + P3_TO_XYZ[0][2] * bLin;
const y = P3_TO_XYZ[1][0] * rLin + P3_TO_XYZ[1][1] * gLin + P3_TO_XYZ[1][2] * bLin;
const z = P3_TO_XYZ[2][0] * rLin + P3_TO_XYZ[2][1] * gLin + P3_TO_XYZ[2][2] * bLin;
const sr = XYZ_TO_SRGB[0][0] * x + XYZ_TO_SRGB[0][1] * y + XYZ_TO_SRGB[0][2] * z;
const sg = XYZ_TO_SRGB[1][0] * x + XYZ_TO_SRGB[1][1] * y + XYZ_TO_SRGB[1][2] * z;
const sb = XYZ_TO_SRGB[2][0] * x + XYZ_TO_SRGB[2][1] * y + XYZ_TO_SRGB[2][2] * z;
const min = -GAMUT_EPSILON;
const max = 1 + GAMUT_EPSILON;
return sr >= min && sr <= max && sg >= min && sg <= max && sb >= min && sb <= max;
}

Use the display-p3 color space on a canvas to render the full P3 gamut:

const ctx = canvas.getContext('2d', { colorSpace: 'display-p3' });
for (let y = 0; y < height; y++) {
const value = 1 - y / (height - 1);
for (let x = 0; x < width; x++) {
const saturation = x / (width - 1);
const [r, g, b] = hsvToRgb(hsvHue, saturation, value);
ctx.fillStyle = `color(display-p3 ${r} ${g} ${b})`;
ctx.fillRect(x, y, 1, 1);
}
}

See Constants and Helpers for all available building blocks.