Contrast Safe Swatch
Picks a readable foreground from real relative luminance, and reports all three WCAG thresholds instead of one badge.
DOM + CSS0 deps1 file · 5 KB TSX
Props
"use client";
import { useMemo, useState } from "react";
/**
* Contrast, computed the way the spec defines it rather than the way it is
* usually approximated.
*/
function rgb(hex: string): [number, number, number] {
const h = hex.replace("#", "");
const n = parseInt(h.length === 3 ? h.split("").map((c) => c + c).join("") : h, 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
/**
* RELATIVE LUMINANCE — the linearised, Rec.709-weighted sum.
*
// ---------------------------------------------------------------
// Full source available with a viberdy Pro subscription.
// https://viberdy.dev/pricing
// ---------------------------------------------------------------
}Details
About this component
Choosing readable text over a colour looks like a solved problem and is routinely solved wrongly. The common shortcut is to read HSL lightness, or average the channels, and put white on anything below fifty percent — but neither is luminance. A saturated yellow and a saturated blue can share an HSL lightness of fifty while their relative luminances differ by more than an order of magnitude, so contrast computed that way cheerfully passes colour pairs nobody can read. The correct calculation linearises each channel through the sRGB transfer function, weights them by the Rec.709 coefficients, and forms the ratio with the specified 0.05 offsets. The foreground is then chosen by computing BOTH candidate ratios and keeping the larger, rather than by thresholding luminance at a half — that trick picks the wrong side across a wide band around mid-grey, which is exactly where the eye is least able to check the answer. And the result is reported against three thresholds rather than one, because 4.5:1 governs body text, 3:1 is sufficient for large text, and 3:1 separately governs non-text contrast like borders and focus rings; collapsing them into one badge either blocks a legal headline or waves through unreadable copy.
Curator’s note
HSL lightness is not luminance. A yellow and a blue can share a lightness of 50% and differ in luminance by an order of magnitude.


