Start
Install color-kit, parse a color, and answer the two questions every color tool asks of it.
Install
pnpm add @pbroom/color-kit@nextcolor-kit is published under the next tag until 1.0. React is optional:
only @pbroom/color-kit/react needs it.
Parse a color
Every value is an OKLCH Color: lightness l from 0 to
1, chroma c, hue h in degrees, and alpha. parse
reads any CSS Color 4 string.
import { parse } from '@pbroom/color-kit';
const coral = parse('oklch(0.7 0.25 30)');
// → { l: 0.7, c: 0.25, h: 30, alpha: 1 }Can a display show it?
coral is outside sRGB and outside Display P3. color-kit keeps the color you
asked for and maps a copy for the display, reducing chroma only, so lightness
and hue hold still.
import { inSrgbGamut, toCss, toSrgbGamut } from '@pbroom/color-kit';
inSrgbGamut(coral); // → false
const shown = toSrgbGamut(coral);
toCss(shown, 'oklch'); // → 'oklch(0.7 0.1915 30)'Is it readable?
Measure contrast on the color you paint.
import { contrastRatio, parse } from '@pbroom/color-kit';
contrastRatio(shown, parse('#fff')).toFixed(2); // → '2.90'2.90:1 fails WCAG AA for body text (4.5:1) and for large text (3:1). Try
your own colors. The example paints hex, so it measures with
{ precision: '8bit' }, and it blends a translucent color over each
background before measuring, since contrastRatio ignores alpha:
- parse
- oklch(0.7 0.25 30)
- inSrgbGamut
- false
- inP3Gamut
- false
- toSrgbGamut
- oklch(0.7 0.1915 30)
- vs white
- 2.90:1
- vs black
- 7.23:1
import { useState } from 'react';
import {
contrastRatio,
inP3Gamut,
inSrgbGamut,
mix,
parse,
toCss,
toSrgbGamut,
tryParse,
type Color,
} from '@pbroom/color-kit';
import { Chip, Readout } from './controls';
const WHITE = parse('#fff');
const BLACK = parse('#000');
/**
* Contrast of `color` painted over an opaque `background`. A translucent
* color shows the background through it, so composite first: source-over in
* gamma-encoded sRGB, the way browsers blend by default, is a plain sRGB mix
* from the background toward the opaque color by its alpha. The chip paints
* hex, so measure 8-bit channels to match what is on screen.
*/
function contrastOver(color: Color, background: Color): string {
const painted = mix(background, { ...color, alpha: 1 }, color.alpha, {
space: 'srgb',
});
const ratio = contrastRatio(painted, background, { precision: '8bit' });
return `${ratio.toFixed(2)}:1`;
}
export default function QuickStart() {
const [input, setInput] = useState('oklch(0.7 0.25 30)');
const color = tryParse(input);
const shown = color ? toSrgbGamut(color) : null;
return (
<div className="grid gap-4">
<input
aria-label="CSS color"
value={input}
onChange={(event) => setInput(event.currentTarget.value)}
spellCheck={false}
className="w-full rounded-sm border border-border bg-transparent px-2 py-1 [font-family:var(--font-mono)] text-[13px] text-foreground"
/>
{color && shown ? (
<div className="flex items-start gap-4">
<Chip css={toCss(shown, 'hex')} label="Displayed in sRGB" />
<Readout
rows={[
['parse', toCss(color, 'oklch')],
['inSrgbGamut', String(inSrgbGamut(color))],
['inP3Gamut', String(inP3Gamut(color))],
['toSrgbGamut', toCss(shown, 'oklch')],
['vs white', contrastOver(shown, WHITE)],
['vs black', contrastOver(shown, BLACK)],
]}
/>
</div>
) : (
<p className="m-0 text-[13px] text-muted-foreground">
<code>tryParse</code> returned <code>null</code>: not a CSS color.
</p>
)}
</div>
);
}Next
- OKLCH-canonical colors and Requested vs displayed explain the model.
- Contrast shows how to find the readable edge as a region instead of by trial.
- Building a picker? Start with React or the framework-free headless picker.