npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@osdlc/color

v1.1.0

Published

sRGB colour arithmetic: parse the CSS colour syntaxes a browser reports, mix and composite in gamma-encoded sRGB, measure WCAG 2 relative luminance and contrast, and the sRGB distance between colours. No dependencies.

Readme

@osdlc/color

sRGB colour arithmetic for checking colour contrast. Parse the colours a browser reports, mix and composite them as a browser does, and measure WCAG 2 contrast and the sRGB distance between two colours. No dependencies.

Why

A contrast check has to measure the colour on the screen. Three things get in the way:

  • A browser reports colours in more than one syntax. getComputedStyle gives rgb(…) for most colours, but color(srgb …), with 0-1 channels, for a colour made with color-mix().
  • A translucent colour never paints its own channels. It paints a mix with what is under it.
  • The browser stores that mix as 8-bit channels, so the painted pixel is a rounded number.

This package reads the syntaxes a browser reports into one value, Rgba, with channels 0-255 and alpha 0-1. It mixes the way color-mix(in srgb, …) does, composites the way a browser paints, rounded to 8 bits, and gives the WCAG 2 contrast ratio. It throws on input it cannot read, so a typo does not become a wrong contrast number.

It is for tools that check the colours of a user interface: theme and design token builds, contrast tests that run in a headless browser, and lint scripts.

When not to use it:

  • For named colours (red), hsl(), hwb(), or other colour spaces such as oklch() or display-p3. Use culori (see below).
  • To convert between colour spaces, or to mix in a perceptual space.
  • For APCA or other contrast models than WCAG 2.

Install

npm install @osdlc/color

pnpm add @osdlc/color and yarn add @osdlc/color work too.

ESM only. Node.js 22 or later.

Quick start

import { compositeOver, contrastRatio, formatHex, mixColors, parseColor } from '@osdlc/color'

// Contrast of two CSS colours.
contrastRatio('#777777', '#ffffff') // 4.478..., just under the 4.5 AA bar

// A translucent text colour, measured as the pixel it paints on white.
const white = parseColor('#ffffff')
const painted = compositeOver(parseColor('rgba(0, 0, 0, 0.6)'), white)
painted // { r: 102, g: 102, b: 102, a: 1 }
contrastRatio(painted, white) // 5.74...

// A tint, as color-mix(in srgb, #ffffff, #1a73e8 25%) computes it.
formatHex(mixColors(white, parseColor('#1a73e8'), 0.25)) // '#c6dcf9'

API

No function changes its arguments.

  • Rgba is a colour: r, g, b 0-255 and a 0-1. The fields are readonly, so frozen objects and as const literals can be passed in.
  • parseColor(css) reads hex (#rgb, #rgba, #rrggbb, #rrggbbaa), rgb() and rgba() in the comma and the space form, and color(srgb r g b [/ a]). Channels are clamped to 0-255 and alpha to 0-1, so rgb(300 0 0) and color(srgb 2 0 0) both give r 255. A browser clamps rgb() when it computes the value; it keeps an out-of-range color(srgb …) as written, so there the clamp is this package's choice. Anything else throws: named colours, hsl(), none, extra arguments such as rgb(1 2 3 4 5), and text such as rgb(12abc 0 0).
  • formatHex(color) writes #rrggbb, or #rrggbbaa when the alpha is below
    1. Channels are rounded and clamped.
  • mixColors(base, overlay, amount) blends amount (0-1) of overlay into base. For opaque colours it gives what color-mix(in srgb, base, overlay <amount>%) computes.
  • withAlpha(color, alpha) replaces the alpha.
  • fromPixel(px) reads an [r, g, b, a] pixel from ImageData.data, where alpha is 0-255.
  • compositeOver(src, ground) paints src over an opaque ground and rounds each channel to 8 bits. A src alpha outside 0-1 is clamped. A src with alpha exactly 1 is returned as is.
  • relativeLuminance(color) is the WCAG 2 relative luminance, 0-1.
  • contrastRatio(a, b) is the WCAG 2 contrast ratio, 1-21. Each side is an Rgba or a CSS string.
  • colorDistance(a, b) is the Euclidean distance over the 0-255 r, g and b channels: 0 for the same colour, about 441.7 for black to white. Each side is an Rgba or a CSS string. Use it to pick the nearest colour of a fixed palette, such as a terminal's 256 colours. It is not a perceptual difference.

relativeLuminance, contrastRatio and colorDistance ignore alpha. Composite a translucent colour with compositeOver first.

import { colorDistance, fromPixel, parseColor, relativeLuminance, withAlpha } from '@osdlc/color'

parseColor('color(srgb 0.5 0.5 0.5 / 50%)') // { r: 127.5, g: 127.5, b: 127.5, a: 0.5 }
withAlpha(parseColor('#1a73e8'), 0.5) // { r: 26, g: 115, b: 232, a: 0.5 }
fromPixel(new Uint8ClampedArray([0, 0, 0, 255])) // { r: 0, g: 0, b: 0, a: 1 }
relativeLuminance(parseColor('#ffffff')) // 1
colorDistance('#000000', 'rgb(3 4 0)') // 5

Errors

There is no error class. Every message starts with color:.

  • parseColor throws an Error for input it cannot read. So does contrastRatio and colorDistance for a string side, and fromPixel for fewer than 4 entries.
  • mixColors and withAlpha throw a RangeError when amount or alpha is NaN or infinite. A finite value outside 0-1 is clamped.
  • compositeOver throws a RangeError when ground.a is below 1. Composite the ground over its own background first.

Each export has TSDoc in the source, with @throws. Your editor shows it.

Measured against a browser

The tests compare the package with values recorded in headless Chromium: getComputedStyle for each syntax parseColor reads, and for inputs the browser rejects; color-mix(in srgb, …) for mixColors; and canvas pixels for compositeOver. compositeOver matches the canvas to within one step per channel. The step comes from Chromium rounding the alpha to 8 bits before it blends; this package blends with the alpha as given.

How it compares

culori does nearly all of this and much more. These results are from culori 4.0.2 on Node.js 24:

| | @osdlc/color | culori 4.0.2 | |---|---|---| | Syntaxes read | hex, rgb()/rgba(), color(srgb …) | all of CSS Color 4, with named colours, hsl() and other colour spaces | | Channels | 0-255, alpha 0-1 | 0-1 | | Bad input | throws an Error | parse returns undefined; rgb(12abc 0 0) throws an internal TypeError | | rgb(300 0 0) | clamped to 255 | kept as is | | Mix in sRGB | mixColors | interpolate([a, b], 'rgb')(t) | | Composite | compositeOver, rounded to 8 bits: 50% black on white gives 128 | blend, not rounded: 0.5 (127.5 of 255); also takes a translucent ground | | Luminance and contrast | relativeLuminance, contrastRatio | wcagLuminance, wcagContrast, with the same results: #777777 on white is 4.4781 in both | | Distance | colorDistance, over 0-255 channels | differenceEuclidean('rgb'), over 0-1 channels | | Canvas pixels | fromPixel | none | | Hex out | formatHex | formatHex, formatHex8 |

Pick culori when you need named colours, hsl(), other colour spaces, or perceptual mixing. Pick this package when you need the 8-bit painted pixel, 0-255 channels, and errors on bad input.

Other libraries:

  • colord parses and mixes, and its a11y plugin gives contrast.
  • chroma-js, tinycolor2 and color parse, mix and give luminance.
  • wcag-contrast and color-contrast-checker give contrast only.

None of them composites with the 8-bit rounding a browser paints.

Stability

Every export is stable from 1.0.0. A breaking change to any of them needs a new major version and is marked Breaking: in the CHANGELOG.

The experimental exports:

  • none

Runtimes and platforms

Node.js 22 or later, ESM only. The built package is tested on Node.js 22 and 24. The code is plain ES2022 arithmetic with no imports and no DOM, but other runtimes, such as Bun, Deno or a browser, are not tested. It has no file system or platform code, so it runs on any operating system. Its tests run on Linux and macOS.

Licence

MIT OR Apache-2.0, at your option. See LICENSE-MIT and LICENSE-APACHE.