Skip to content

Color helpers ​

Six helpers for the color arithmetic an admin screen does: parsing, contrast, and picking text that can actually be read.

Open in demo
Open in demo

Import ​

ts
import { colorFromString, contrastColor, contrastRatio, hexToRgb, isLightColor, rgbToHex } from '@shared/utils/color'

hexToRgb ​

ts
function hexToRgb(hex: string): Rgb | null

interface Rgb {
  r: number // 0–255
  g: number // 0–255
  b: number // 0–255
  a: number // 0–1, defaults to 1
}
ts
hexToRgb('#3b82f6') // { r: 59, g: 130, b: 246, a: 1 }
hexToRgb('#f0a') // { r: 255, g: 0, b: 170, a: 1 }

Returns null when it cannot parse. A bad value must fail visibly rather than resolve to black. Three-, six- and eight-digit hex all work.

rgbToHex ​

ts
function rgbToHex(rgb: Rgb): string
function rgbToHex(r: number, g: number, b: number): string
ts
rgbToHex(59, 130, 246) // '#3b82f6'
rgbToHex({ r: 59, g: 130, b: 246, a: 1 }) // '#3b82f6'

Channels are rounded and clamped, so arithmetic that overshoots still yields a valid color. Alpha is dropped.

contrastRatio ​

ts
function contrastRatio(foreground: string, background: string): number
ts
contrastRatio('#ffffff', '#3b82f6') // 3.68

The WCAG ratio, 1 (identical) to 21 (black on white), and 0 if either color cannot be parsed.

AA is 4.5 for body text, 3 for large text and UI components. The example above passes for large text only.

isLightColor ​

ts
function isLightColor(color: string): boolean
ts
isLightColor('#fde047') // true
isLightColor('#1e293b') // false

Defined as "black contrasts better than white here" — the same criterion contrastColor uses, not a perceptual lightness test. That is why a mid-dark blue can count as light: what matters is which text will be readable, not how bright the color looks.

contrastColor ​

ts
function contrastColor(background: string, options?: ContrastColorOptions): string

interface ContrastColorOptions {
  light?: string // returned on dark backgrounds, default '#ffffff'
  dark?: string // returned on light backgrounds, default '#000000'
}
ts
contrastColor('#fde047') // '#000000'
contrastColor('#1e293b') // '#ffffff'

Whichever candidate wins on WCAG contrast. An unparseable background falls back to dark, which stays readable where the reverse would not.

colorFromString ​

ts
function colorFromString(value: string, options?: ColorFromStringOptions): string

interface ColorFromStringOptions {
  saturation?: number // 0–100, default 65
  lightness?: number // 0–100, default 55
}
ts
colorFromString('Ada Lovelace') // the same hex on every render
colorFromString('محمد المعراج') // works on any script

For avatar tiles and tag chips. Only the hue varies, which is what keeps a wall of avatars looking like one palette rather than a bag of sweets. The hash walks code points, so an astral character counts once.

Pair it with contrastColor for the initials on top, and initials for the letters themselves.