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

@luntta/swatch

v3.0.1

Published

A color library with first-class colorblind support, CSS Color 4 parsing, wide-gamut spaces, OKLCh manipulation, color scales, built-in palettes, blend modes, WCAG 2.1, and APCA.

Readme

Swatch

A color library with first-class colorblind support — Machado CVD simulation, severity continuum, Fidaner daltonization, and palette distinguishability checks — plus the things you expect from a modern color library: CSS Color 4 parsing, wide-gamut spaces, OKLab/OKLCh manipulation, color scales, built-in scientific palettes, blend modes, naming, temperature, WCAG 2.1, and APCA.

Docs & interactive playground: https://luntta.github.io/swatch/

3.0 is a breaking rewrite. See MIGRATING.md for the v2 → v3 cookbook. The most visible change is that lighten/darken/saturate now operate in OKLCh with amounts in 0..1 instead of HSL with amounts in 0..100.

Install

npm install @luntta/swatch
import swatch from "@luntta/swatch";

The factory statics are also available as named exports, so bundlers can tree-shake the ones you don't use:

import { scale, contrast, simulate } from "@luntta/swatch";

Quick start

const blue = swatch("#3b82f6");

blue.lighten(0.1).hex();                         // perceptual OKLCh lighten
blue.contrast("#ffffff");                        // WCAG 2.1 contrast ratio
blue.isReadable("#ffffff");                      // AA body-text default
blue.simulate(swatch.cvd.deutan).hex();          // colorblind simulation

swatch.scale(["#f00", "#00f"]).colors(5, "hex"); // theme/data-viz ramps
swatch.try(userInput);                           // Swatch | null

Creating a color

swatch(input) accepts any of the usual CSS forms — including CSS Color 4 — object literals, or another swatch.

// Legacy CSS.
swatch("#0000ff");
swatch("#00f");
swatch("#0000ff80");                   // with alpha
swatch("rgb(0, 0, 255)");
swatch("rgba(0, 0, 255, 0.5)");
swatch("hsl(240, 100%, 50%)");
swatch("rebeccapurple");               // all 148 CSS named colors

// CSS Color 4.
swatch("rgb(0 0 255 / 0.5)");          // modern slash-alpha
swatch("hsl(240 100% 50% / 50%)");
swatch("hwb(240 0% 0%)");
swatch("lab(52.2% 40 -70)");
swatch("lch(52.2% 80.5 300)");
swatch("oklab(0.5 0.1 -0.2)");
swatch("oklch(0.7 0.15 240)");
swatch("color(display-p3 1 0 0)");     // wide-gamut
swatch("color(rec2020 1 0 0)");
swatch("color(xyz-d65 0.4124 0.2126 0.0193)");

// Objects.
swatch({ r: 0, g: 0, b: 255 });
swatch({ h: 240, s: 100, l: 50, a: 0.5 });
swatch({ space: "oklch", coords: [0.7, 0.15, 240] });

Invalid input throws. For live text inputs and validation, use the safe helpers:

const c = swatch.try("not a color");   // null
swatch.isColor("oklch(0.7 0.15 240)"); // true

Instances are immutable — every operation returns a new swatch.

Common serialization helpers are available for the everyday cases:

const c = swatch("rgb(51 102 204 / 0.5)");

c.hex();                  // "#3366cc"
c.hex({ alpha: true });   // "#3366cc80"
c.rgb();                  // { r: 51, g: 102, b: 204, a: 0.5 }
c.css({ format: "oklch" });

hex() and rgb() map wide-gamut colors into sRGB perceptually (CSS Color 4 chroma reduction) before serializing, so they never silently clip chromaticity. Pass { gamut: false } for a raw clamp, or use css() to keep the color in its source space losslessly. See Gamut mapping.

Color spaces

Each instance exposes a getter for every registered space. Conversions go through CIE XYZ D65 and are lazily memoized on the instance.

const c = swatch("oklch(0.7 0.15 240)");

c.srgb;        // { r, g, b }     — may be out of [0,1] for wide-gamut sources
c.linearSrgb;  // { r, g, b }
c.hsl;         // { h, s, l }
c.hsv;         // { h, s, v }
c.hwb;         // { h, w, b }
c.cmyk;        // { c, m, y, k }  — naive, uncalibrated
c.lab;         // { l, a, b }     — CIE Lab D65
c.lch;         // { l, c, h }
c.oklab;       // { l, a, b }
c.oklch;       // { l, c, h }
c.hsluv;       // { h, s, l }     — human-friendly HSL
c.luv;         // { l, u, v }
c.xyz;         // { x, y, z }     — D65
c.displayP3;   // { r, g, b }
c.rec2020;     // { r, g, b }
c.a98;         // { r, g, b }
c.prophoto;    // { r, g, b }

Getters return mathematically correct coordinates — they do not gamut-map. For a wide-gamut source like color(display-p3 1 0 0), .srgb will include out-of-range values. Use .toGamut() if you need an in-gamut sRGB version.

Switch the canonical space with c.to(spaceId):

swatch("#ff0000").to("oklch");
// Swatch { space: "oklch", coords: [0.628, 0.258, 29.2], alpha: 1 }

For autocomplete-friendly call sites, string constants are exposed:

swatch("#ff0000").to(swatch.spaces.oklch);
swatch("#ff0000").simulate(swatch.cvd.protan);

Units cheat sheet

| API surface | Units | | --- | --- | | c.srgb, c.displayP3, color(display-p3 ...) | 0..1 RGB channels | | c.rgb() and { r, g, b } input | 0..255 RGB channels | | hsl, hsv, hwb, hsluv | hue degrees, percentages for the other channels | | oklab.l, oklch.l | 0..1 perceptual lightness | | lab.l, lch.l, luv.l | 0..100 CIE lightness | | lighten / darken | delta in OKLCh L (0..1) | | saturate / desaturate | delta in OKLCh C |

Channels

Address any channel with a string path:

const c = swatch("oklch(0.7 0.15 240)");
c.get("oklch.l");   // 0.7
c.get("hsl.h");     // 240 (converted from oklch)
c.get("alpha");     // 1

c.set("oklch.h", 120);  // returns a new Swatch with hue = 120°

Channel paths are typed in TypeScript, so typos like "oklch.lightness" are caught by the compiler.

Manipulation

Breaking change from v2. Manipulation now operates in OKLCh (perceptually uniform) instead of HSL. Amounts are in 0..1, not 0..100.

swatch("#ff0000").lighten(0.1);      // +0.1 in OKLCh L
swatch("#ff0000").darken(0.1);
swatch("#ff0000").saturate(0.05);    // +0.05 in OKLCh C
swatch("#ff0000").desaturate(0.05);
swatch("#ff0000").spin(180);         // +180° OKLCh hue
swatch("#ff0000").greyscale();       // OKLCh C = 0
swatch("#ff0000").complement();
swatch("#ff0000").invert();          // per-channel sRGB invert

OKLCh-based lightening means #ffff00 and #0000ff both become visually brighter by the same amount — unlike HSL, where yellow barely changed at all.

Results are re-mapped to sRGB after each operation. Pass { gamut: false } to opt out.

Tint, shade, tone

Mix toward white / black / mid-grey in OKLab.

swatch("#ff0000").tint(0.2);    // 20% toward white
swatch("#ff0000").shade(0.2);   // 20% toward black
swatch("#ff0000").tone(0.2);    // 20% toward 50% grey

Mix and blend

const a = swatch("#ff0000");
const b = swatch("#0000ff");

a.mix(b);                            // oklab, 50/50 (default)
a.mix(b, 0.25);                      // weighted 25% toward b
a.mix(b, 0.5, { space: "lab" });
a.mix(b, 0.5, { space: "oklch" });   // polar, shortest-arc hue
a.mix(b, 0.5, { space: "srgb" });    // naive gamma-space blend

W3C Compositing and Blending Level 1 blend modes:

swatch("#ff0000").blend("#0000ff", "multiply");
swatch("#ff0000").blend("#00ff00", "screen");
// Also: darken, lighten, overlay, color-dodge, color-burn,
//       hard-light, soft-light, difference, exclusion, normal

Color scales

Chroma.js-style scale builder. Interpolates between stops in OKLab by default.

const s = swatch.scale(["#ff0000", "#ffff00", "#00ff00"]);

s(0);                  // Swatch at the start
s(0.5);                // midpoint
s.colors(5);           // [Swatch, Swatch, Swatch, Swatch, Swatch]
s.colors(5, "hex");    // ["#ff0000", "#ffa000", "#ffff00", "#b0ff00", "#00ff00"]

Shape the curve:

swatch.scale(["#000", "#fff"])
  .domain([0, 100])          // remap the input range
  .padding([0.1, 0.1])       // trim fractional amounts off each end
  .gamma(0.5)                // pow the normalized t (emphasis curve)
  .classes(5)                // bucket into 5 discrete bins
  .mode("oklch")             // interpolation space
  .correctLightness()        // resample so the output Lab L is linear
  .cache(true);              // memoize samples (default)

Interpolators that feed into .scale(...):

swatch.bezier(["#ff0000", "#ffff00", "#00ff00"]);
swatch.cubehelix({ start: 300, rotations: -1.5, hue: 1, lightness: [0, 1] });

Built-in palettes

Matplotlib perceptually-uniform colormaps (Smith & van der Walt) and the full ColorBrewer 2.0 set (Cynthia Brewer).

swatch.scale("viridis");
swatch.scale("magma");
swatch.scale("plasma");
swatch.scale("inferno");
swatch.scale("cividis");      // colorblind-safe by design

swatch.scale("Blues");
swatch.scale("RdBu");
swatch.scale("Spectral");
swatch.scale("Set1").classes(9);  // qualitative → discrete

swatch.palettes();            // all registered palette names

Gamut mapping

const p3red = swatch("color(display-p3 1 0 0)");

p3red.gamut;                  // "display-p3" — smallest standard gamut that fits
p3red.inGamut("srgb");        // false
p3red.toGamut({ space: "srgb" });
p3red.toGamut({ space: "srgb", method: "clip" });       // naive clipping
p3red.toGamut({ space: "srgb", method: "css4" });       // CSS Color 4 binary chroma reduction (default)

.gamut reports the smallest standard gamut containing the color, walking srgb ⊂ display-p3 ⊂ rec2020 ⊂ prophoto (or null for imaginary colors). It's a quick way to notice an out-of-sRGB color before serializing.

The .srgb / .linearSrgb / .hsl getters return raw conversions, so wide-gamut sources can produce out-of-range values — they give you the exact math, not a display-ready color. The presentation helpers are gamut-aware: c.hex() and c.rgb() perceptually map into sRGB first (pass { gamut: false } for the old raw clamp). c.toString({ format: "hex" }) / c.toCss(...) still clip naively. Reach for toGamut directly when you want to control the target space or method.

Colorblind simulation

Physiologically based Machado/Oliveira/Fernandes CVD simulation in linear-light sRGB.

const red = swatch("#ff0000");

red.simulate("protan");                       // full protanopia
red.simulate("deutan", { severity: 0.6 });    // 60% deuteranomaly
red.simulate("tritan");
red.simulate("achroma");                      // Rec. 709 grayscale

Accepted aliases: protanopia/protanomalyprotan, deuteranopia/deuteranomalydeutan, tritanopia/tritanomalytritan, achromatopsiaachroma.

Severity 0.0 is identity, 1.0 is the full dichromat, and in-between values interpolate across the published Machado severity table.

ImageData simulation

For photos and other raster images, use the batch ImageData API instead of calling swatch(pixel).simulate(...) in a loop. It uses byte/linear-light LUTs and computes the CVD matrix once per image.

const ctx = canvas.getContext("2d");
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

swatch.simulateImageData(imageData, "deutan", { severity: 1 });
ctx.putImageData(imageData, 0, 0);

The transform mutates in place by default for performance. Pass { inPlace: false } to clone first. Alpha is preserved.

Daltonization

Fidaner error-redistribution: the information lost to a dichromat is shifted into channels they can still see.

swatch("#ff0000").daltonize("deutan");
swatch("#ff0000").daltonize("protan", { severity: 0.8 });

The same batch API is available for raster corrections:

swatch.daltonizeImageData(imageData, "protan");

Palette distinguishability

const report = swatch.checkPalette(
  ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00"],
  { cvd: "deutan", minDeltaE: 11 }
);

report.safe;        // false
report.minDeltaE;   // e.g. 7.2
report.unsafe;      // [{ i, j, deltaE, safe: false }, …]

Nudge a single color until it clears a threshold against a reference:

swatch.nearestDistinguishable("#ff6666", "#ff0000", {
  cvd: "deutan",
  minDeltaE: 15
});

WCAG 2.1 contrast

swatch.contrast("#ffffff", "#000000");      // 21
swatch("#767676").isReadable("#ffffff");    // true (AA normal body text)
swatch("#767676").isReadable("#ffffff", { level: "AAA", size: "normal" });
swatch("#767676").isReadable("#ffffff", { level: "AA",  size: "large"  });

Auto-fix a foreground until it meets a target ratio:

swatch("#888888").ensureContrast("#ffffff", { minRatio: 4.5 });

Pick the best candidate from a list:

swatch.mostReadable("#ffffff", ["#888", "#555", "#222"]);
// → Swatch("#222222")

APCA

Andrew Somers' SAPC-based contrast. Returns signed Lc on a ~[-108, +106] scale.

swatch.apcaContrast("#000000", "#ffffff");    //  106.04  (BoW)
swatch.apcaContrast("#ffffff", "#000000");    // -107.88  (WoB)
swatch.apcaContrast("#767676", "#ffffff");    //   71.57  (fails body-text min)

Positive = dark text on a light background; negative = light text on a dark background. Typical body-text thresholds: |Lc| ≥ 75 comfortable, ≥ 60 minimum.

Delta E

const a = swatch("#3366cc");
a.deltaE("#3366cd");                          // ΔE2000 (default)
a.deltaE("#3366cd", "76");                    // ΔE76
a.deltaE("#3366cd", "94");                    // ΔE94 graphic arts
a.deltaE("#3366cd", "cmc", { l: 2, c: 1 });   // CMC l:c (2:1 acceptability)
a.deltaE("#3366cd", "hyab");                  // HyAB (large differences)
a.deltaE("#3366cd", "ok");                    // ΔE OK

Color naming

swatch("#dc143c").name();
// { name: "crimson", hex: "#dc143c", deltaE: 0 }

swatch("#dc1b3d").toName();
// "crimson"

Nearest match from the 148 CSS Color Module 4 named colors, measured in ΔE2000.

Temperature

swatch.temperature(6500);       // D65-ish white
swatch.temperature(2000);       // warm candlelight orange
swatch("#ffb070").temperature();// → ~2870 K (McCamy approximation)

Krystek 1985 blackbody for kelvin → color; McCamy 1992 for the inverse.

Random

swatch.random();

swatch.random({
  space: "oklch",
  lightness: [0.6, 0.8],
  chroma: [0.1, 0.2],
  hue: [180, 240],
  seed: 42
});

Pass seed for reproducible sequences (xorshift32).

TypeScript

Hand-written declarations ship in types/swatch.d.ts and are referenced by package.json. No configuration required. Channel paths and constants are typed for autocomplete:

const c = swatch("#3366cc");
c.get("oklch.l");          // ok
c.to(swatch.spaces.oklch); // ok
// c.get("oklch.lightness"); // TypeScript error

Development

npm install
npm test
npm run docs:dev
npm run check
npm run pack:check

See CONTRIBUTING.md for the repo layout, docs workflow, and release checklist.

License

MIT