Skip to content

Contrast Safe Swatch

Picks a readable foreground from real relative luminance, and reports all three WCAG thresholds instead of one badge.

Prominimaleditorial
Category
Components
Added
2026-09-21
Deps
none
Updated
2026-09-21

This is a viberdy Pro component

The live preview, the props panel and the write-up are open to everyone. The full source, the AI prompt and CLI install are part of Pro.

Preview

The hinge, the handle, the moment a thing answers back.

The hinge, the handle, the moment a thing answers back.

Foreground chosen
#000000
Contrast ratio
18.16:1
✓ AA body 4.5:1✓ AA large 3:1✓ AAA body 7:1✓ UI / focus 3:1

Props

No runtime dependencies
"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
  // ---------------------------------------------------------------
}
Notes

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.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Featured
NewPro

Pulls a genuinely distinct palette out of a photograph by median cut — splitting on channel spread, not on population, sorted by relative luminance.

canvascolorimage
Featured
NewPro

A button that survives a real round trip — a minimum pending duration, a width that never twitches, aria-disabled instead of disabled.

buttonasyncloading
Featured
NewPro

A sheet on the native dialog element — no hand-rolled focus trap, and the three things the platform leaves you fixed.

dialogmodalsheet