@absoluteui/helpers/color
Colour parsing and conversion (hex, rgb, hsl, oklch) and WCAG contrast checks.
import { ... } from '@absoluteui/helpers/color';How to use
Section titled “How to use”Parse any CSS colour, convert between hex, rgb(), hsl(), and oklch(), derive shades, and check WCAG contrast. Reach for it when colours come from users or data (department badges, calendar categories, brand settings) and text on them must stay readable. The Theming guide covers design tokens; this module works on single colour values.
How do I pick readable text for a colour a user chose?
Section titled “How do I pick readable text for a colour a user chose?”bestTextColor returns black or white (or the best of your own candidates) for the background, and meetsContrast confirms the pair passes WCAG AA.
import { bestTextColor, meetsContrast } from '@absoluteui/helpers/color';
const badge = (background: string) => { const text = bestTextColor(background, ['#111827', '#ffffff']); return { background, color: text, readable: meetsContrast(text, background) };};badge('#1e3a8a'); // => { background: '#1e3a8a', color: '#ffffff', readable: true }badge('#fde68a'); // => { background: '#fde68a', color: '#111827', readable: true }How do I check a set of theme colours against WCAG?
Section titled “How do I check a set of theme colours against WCAG?”contrastRatio gives the unrounded ratio; meetsContrast applies the threshold for the level and size you need: AA 4.5, AA large text 3, AAA 7, and 3 for non-text elements such as focus rings and input borders.
import { contrastRatio, meetsContrast } from '@absoluteui/helpers/color';
const pairs = [ { name: 'body text', fg: '#374151', bg: '#ffffff', size: 'normal' }, { name: 'placeholder', fg: '#9ca3af', bg: '#ffffff', size: 'normal' }, { name: 'focus ring', fg: '#2563eb', bg: '#ffffff', size: 'non-text' },] as const;const failing = pairs.filter((p) => !meetsContrast(p.fg, p.bg, { size: p.size })).map((p) => p.name);failing; // => ['placeholder']Number(contrastRatio('#9ca3af', '#ffffff').toFixed(2)); // => 2.54How do I derive hover, pressed, and tint shades from one brand colour?
Section titled “How do I derive hover, pressed, and tint shades from one brand colour?”darken and lighten move OKLCH lightness, so the hue stays the same to the eye; withAlpha makes a translucent tint and mix blends two colours. Format the result for CSS with formatHex, formatRgb, or formatOklch.
import { contrastRatio, darken, formatHex, formatRgb, lighten, mix, withAlpha } from '@absoluteui/helpers/color';
const brand = '#2563eb';const hover = formatHex(darken(brand, 0.08));const pressed = formatHex(darken(brand, 0.16));contrastRatio(pressed, '#fff') > contrastRatio(hover, '#fff'); // => truecontrastRatio(formatHex(lighten(brand, 0.3)), '#fff') < contrastRatio(brand, '#fff'); // => trueformatRgb(withAlpha(brand, 0.12)); // => 'rgb(37 99 235 / 0.12)'formatHex(mix(brand, '#ffffff', 1)); // => '#ffffff'How do I validate and normalise a colour field?
Section titled “How do I validate and normalise a colour field?”parseColor accepts every CSS form (hex, rgb(), hsl(), oklch(), named colours, transparent) and returns undefined for anything else. Store one canonical form, such as lowercase hex.
import { formatHex, parseColor } from '@absoluteui/helpers/color';
const normalise = (input: string) => { const rgb = parseColor(input.trim()); return rgb === undefined ? undefined : formatHex(rgb);};normalise(' RebeccaPurple '); // => '#663399'normalise('hsl(217 91% 60%)'); // => '#3c83f6'normalise('rgb(0 128 0 / 50%)'); // => '#00800080'normalise('blueish'); // => undefinedWhat goes wrong?
Section titled “What goes wrong?”currentcolor and unknown names are not colours here, because they have no value outside the page. Translucent colours change contrast: contrastRatio composites them over the background (and a translucent background over white), so check the colour as it is actually drawn.
import { contrastRatio, parseColor, parseColorOrThrow } from '@absoluteui/helpers/color';
parseColor('currentcolor'); // => undefinedparseColorOrThrow('brand-blue'); // => throws HLP_COLOR_INVALIDcontrastRatio('rgb(0 0 0 / 0.4)', '#ffffff') < contrastRatio('#000000', '#ffffff'); // => trueAPI reference
Section titled “API reference”Functions
Section titled “Functions”bestTextColor
The candidate with the highest contrast against bg (first wins ties).
function bestTextColor(bg: ColorInput): '#000000' | '#ffffff';function bestTextColor<T extends ColorInput>(bg: ColorInput, candidates: readonly T[]): T;Example:
bestTextColor('#1e3a8a'); // => '#ffffff'bestTextColor('#fde68a', ['#1f2937', '#f9fafb']); // => '#1f2937'colorDistance
Perceptual distance ΔEOK (Euclidean in Oklab; ~ 0.02 is a just-noticeable difference). Alpha ignored.
function colorDistance(a: ColorInput, b: ColorInput): number;Example:
colorDistance('#000', '#fff'); // => ≈ 1.00compositeOver
Alpha-composites fg over bg (source-over).
function compositeOver(fg: ColorInput, bg: ColorInput): Rgb;Example:
compositeOver('rgb(0 0 0 / 0.5)', '#ffffff'); // => { r: 127.5, g: 127.5, b: 127.5, alpha: 1 }contrastRatio
WCAG contrast ratio (1-21), unrounded. A translucent background is composited over white and a translucent foreground over the background.
function contrastRatio(fg: ColorInput, bg: ColorInput): number;Example:
contrastRatio('#000', '#fff'); // => 21contrastRatio('#777', '#fff'); // => ≈ 4.48cssColorNames
The CSS named colour keywords (lowercase).
function cssColorNames(): string[];Example:
cssColorNames().length; // => 148darken
Lowers OKLCH lightness by amount (0-1, absolute).
function darken(color: ColorInput, amount?: number, options?: GamutOptions): Rgb;Example:
formatHex(darken('#93c5fd', 0.2));formatHex
Lowercase hex (#rrggbb or #rrggbbaa).
function formatHex(color: ColorInput, options?: FormatHexOptions): string;Example:
formatHex('rgb(255 0 0 / 0.5)'); // => '#ff000080'formatHsl
CSS hsl() in modern syntax.
function formatHsl(color: ColorInput): string;Example:
formatHsl('#ff0000'); // => 'hsl(0 100% 50%)'formatOklch
CSS oklch() with lightness as a percentage.
function formatOklch(color: ColorInput): string;Example:
formatOklch('#ff0000'); // => 'oklch(62.796% 0.25768 29.234)'formatRgb
CSS rgb() in modern space syntax, or legacy rgb(r, g, b)/rgba(...) with { legacy: true }.
function formatRgb(color: ColorInput, options?: { readonly legacy?: boolean;}): string;Example:
formatRgb('#ff000080'); // => 'rgb(255 0 0 / 0.502)'hslToRgb
HSL to sRGB.
function hslToRgb(hsl: Hsl): Rgb;Example:
hslToRgb({ h: 120, s: 100, l: 25, alpha: 1 }); // => { r: 0, g: 127.5, b: 0, alpha: 1 }isDark
True when white text contrasts better than black (translucent colours are composited over white).
function isDark(color: ColorInput): boolean;Example:
isDark('#1e3a8a'); // => trueisLight
Opposite of isDark.
function isLight(color: ColorInput): boolean;Example:
isLight('#fef3c7'); // => truelighten
Raises OKLCH lightness by amount (0-1, absolute).
function lighten(color: ColorInput, amount?: number, options?: GamutOptions): Rgb;Example:
formatHex(lighten('#1e3a8a', 0.2));meetsContrast
WCAG threshold check: AA 4.5 (large 3), AAA 7 (large 4.5), non-text 3. The ratio is not rounded.
function meetsContrast(fg: ColorInput, bg: ColorInput, options?: MeetsContrastOptions): boolean;Example:
meetsContrast('#777', '#fff'); // => false // 4.48 < 4.5meetsContrast('#777', '#fff', { size: 'large' }); // => truemix
Mixes two colours; weight is the share of b (0 -> a, 1 -> b). Alpha is interpolated linearly.
function mix(a: ColorInput, b: ColorInput, weight?: number, options?: MixOptions): Rgb;Example:
formatHex(mix('#000', '#fff', 0.5, { space: 'srgb' })); // => '#808080'oklchToRgb
OKLCH to sRGB with gamut mapping.
function oklchToRgb(lch: Oklch, options?: GamutOptions): Rgb;Example:
const red = oklchToRgb({ l: 0.628, c: 0.2577, h: 29.23, alpha: 1 });red.r; // => ≈ 255red.g; // => ≈ 0red.b; // => ≈ 0parseColor
Parses a CSS colour: hex (#rgb, #rgba, #rrggbb, #rrggbbaa), rgb()/rgba() (legacy
commas and modern space syntax), hsl()/hsla(), oklch(), the 148 named colours, and
transparent. Returns undefined when invalid (including currentcolor).
function parseColor(input: string): Rgb | undefined;Example:
parseColor('rgb(255 0 0 / 50%)'); // => { r: 255, g: 0, b: 0, alpha: 0.5 }parseColor('rebeccapurple'); // => { r: 102, g: 51, b: 153, alpha: 1 }parseColorOrThrow
Like parseColor but throws HLP_COLOR_INVALID.
function parseColorOrThrow(input: string): Rgb;Example:
parseColorOrThrow('#fff'); // => { r: 255, g: 255, b: 255, alpha: 1 }relativeLuminance
WCAG 2.x relative luminance (0 black - 1 white). Alpha is ignored: composite first with compositeOver.
function relativeLuminance(color: ColorInput): number;Example:
relativeLuminance('#ffffff'); // => 1rgbToHsl
sRGB to HSL.
function rgbToHsl(rgb: Rgb): Hsl;Example:
rgbToHsl({ r: 255, g: 0, b: 0, alpha: 1 }); // => { h: 0, s: 100, l: 50, alpha: 1 }rgbToOklch
sRGB to OKLCH (hue 0 for greys).
function rgbToOklch(rgb: Rgb): Oklch;Example:
const white = rgbToOklch({ r: 255, g: 255, b: 255, alpha: 1 });white.l; // => ≈ 1.000white.c; // => ≈ 0.000white.h; // => 0withAlpha
Returns the colour with alpha (0-1) replaced.
function withAlpha(color: ColorInput, alpha: number): Rgb;Example:
formatHex(withAlpha('#ff0000', 0.5)); // => '#ff000080'Interfaces
Section titled “Interfaces”FormatHexOptions
interface FormatHexOptions { readonly alpha?: 'auto' | 'always' | 'never';}GamutOptions
interface GamutOptions { readonly gamut?: 'chroma' | 'clip';}Hsl
Hue 0-360 (0 for greys), saturation and lightness 0-100, alpha 0-1.
interface Hsl { readonly h: number; readonly s: number; readonly l: number; readonly alpha: number;}MeetsContrastOptions
interface MeetsContrastOptions { readonly level?: 'AA' | 'AAA'; readonly size?: 'normal' | 'large' | 'non-text';}MixOptions
interface MixOptions extends GamutOptions { readonly space?: 'oklch' | 'srgb';}Oklch
OKLCH: lightness 0-1, chroma >= 0 (~0-0.4 for sRGB), hue 0-360, alpha 0-1.
interface Oklch { readonly l: number; readonly c: number; readonly h: number; readonly alpha: number;}Rgb
sRGB colour: channels 0-255 (may be fractional), alpha 0-1.
interface Rgb { readonly r: number; readonly g: number; readonly b: number; readonly alpha: number;}ColorInput
A CSS colour string or an RGB object (alpha defaults to 1).
type ColorInput = string | { readonly r: number; readonly g: number; readonly b: number; readonly alpha?: number;};