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

paletteguard

v0.1.0

Published

Find chart colours your colourblind users can't tell apart. Runs in CI.

Readme

paletteguard

Find the chart colours your colourblind users can't tell apart — in CI, before they ship.

CI npm license node


Your revenue line is green. Your expenses line is amber. It looks fine.

To roughly 1 in 12 men, those two lines are the same colour.

You will never get a bug report about it. People squint, guess, misread the chart, and quietly trust your product a little less.

The same three-series chart shown twice — as normal vision sees it, and as a reader with protanopia does, where two of the three lines become nearly the same colour

That is not an illustration. It is d3's default palette, the first three colours, rendered through a colour vision deficiency simulation. Three series is enough.

$ npx paletteguard chart-colors.json

  paletteguard — 4 colours, 18 comparisons

  ✗ FAIL  accent vs danger
         deuteranopia (green-blind)
         seen as #a7a788 · #9b9b5d
         distance 8.96  ·  needs 12.19

  ✗ FAIL  accent vs amber
         protanopia (red-blind)
         seen as #c1c185 · #bbbb41
         distance 9.87  ·  needs 13.38

  ────────────────────────────────────────────
  2 fail · 1 warn · 15 pass

Exit code 1. The build stops.

Quick start

npx paletteguard palette.json      # no install
npm install -D paletteguard        # or keep it around
# see it fail on d3's default, right now
echo '["#1f77b4","#ff7f0e","#2ca02c"]' | npx paletteguard

Contents


Every major charting default fails

I pointed it at the default categorical palettes shipped by the major charting libraries. Every one has pairs a colourblind reader cannot reliably separate.

Libraries hand out colours in order, so the question that matters is not whether a ten-colour palette hides a bad pair somewhere — it is how many series before something breaks.

| Palette | Breaks at | The pair | |---|---|---| | d3 schemeCategory10 | 3 series | #ff7f0e vs #2ca02c — ΔE 4.69, protanopia | | Chart.js defaults | 4 series | #ff9f40 vs #ffcd56 — ΔE 7.57, deuteranopia | | d3 schemeTableau10 | 5 series | #e15759 vs #59a14f — ΔE 2.26, deuteranopia | | d3 schemeObservable10 | 5 series | ΔE 6.01, protanopia | | Wong (reference) | never | — |

The closest pair found anywhere: schemeTableau10's #ff9da7 against #bab0ab, ΔE 1.12 under protanopia. They simulate to #aeaea7 and #b2b2ab. For practical purposes, one colour.

Full audit — with controls, limitations, and the objection I can't rebut →

Why this exists

I built an app, cared about accessibility, did the contrast maths, and shipped a dashboard where success-green and error-red are nearly identical to the most common form of colourblindness. Two people looked at it. Neither saw anything wrong — because you can't. You have to compute it.

Every existing colourblindness tool is a web page you paste colours into. That helps only when a human remembers to check. This is a build step, so it catches the problem when nobody remembers.

Background: what is actually being measured

Colour vision works through three cone types, sensitive to long (L), medium (M) and short (S) wavelengths. Colour vision deficiency is one of those cones being absent or shifted.

| Condition | Cone affected | Commonly called | Roughly | |---|---|---|---| | Protanopia | L (long) | red-blind | red-green family | | Deuteranopia | M (medium) | green-blind | red-green family | | Tritanopia | S (short) | blue-blind | rare |

Around 300 million people worldwide have some form — about 1 in 12 men and 1 in 200 women. Roughly 95% of them are men, because the genes for the L and M cones sit on the X chromosome. Red-green deficiency is ~99% of all cases, which is why paletteguard scores protanopia and deuteranopia more strictly than tritanopia.

An important distinction, and a real limitation of this tool. The conditions above are dichromacies — a cone type missing outright. The milder and more common conditions are anomalous trichromacies (protanomaly, deuteranomaly), where the cone is present but its sensitivity is shifted. paletteguard simulates the dichromacies, so its results are a worst case, not what every colourblind reader experiences.

That is a deliberate choice: a palette that survives the severe case survives the mild one, and the reverse is not true.

How it works

palette → simulate 3 conditions → every pair → CIEDE2000 → compare to floor
  1. Parse the input into named colours.
  2. Simulate each colour under protanopia, deuteranopia and tritanopia, via @bjornlu/colorblind — a zero-dependency implementation of hx2A's colour blindness simulation research.
  3. Compare every pair, under each condition, using CIEDE2000 perceptual distance.
  4. Verdict per pair: pass, warn or fail against a per-condition floor.

Why all pairs

Because a categorical palette is by definition a set of colours meant to be mutually distinguishable. If two of them collide, that is a real defect regardless of which two.

This is exactly why the tool is scoped to palettes and not to design tokens generally: a linter cannot know that --surface-raised and --surface-overlay are never meant to be told apart, so checking all pairs of a token file produces mostly noise. Scoping to categorical palettes makes all-pairs correct by construction.

Why CIEDE2000

Perceptual difference formulas are not interchangeable:

  • CIE76 — plain Euclidean distance in CIELAB. Simple, but badly non-uniform in saturated colours and blues.
  • OKLab distance — modern and well-behaved, but on a scale of roughly 0–0.4, which nobody recognises.
  • CIEDE2000 — the CIE's current standard, with corrections for lightness, chroma and hue interactions. It is what "ΔE" means to most people, on a familiar 0–100 scale.

paletteguard reports CIEDE2000 so its numbers can be compared with other tools. OKLab distance was computed alongside during development and agreed on ordering.

How the threshold is set

Not invented. Measured.

Bang Wong's palette (Nature Methods, 2011) was published specifically as a categorical palette that survives colour vision deficiency. paletteguard computes the smallest gap Wong itself achieves under each condition, and holds your palette to that same bar:

$ npx paletteguard --thresholds

  condition       fail below    pass at or above
  ────────────────────────────────────────────────
  protanopia          10.04               13.38
  deuteranopia         9.14               12.19
  tritanopia           7.34                9.79

The pass floor is derived at runtime, not hardcoded, so it can never drift out of step with the simulation model. The fail floor is 0.75 of it.

That ratio is the single judgement call in the tool, so it is kept visible. Measured across six palettes, the usable window is 0.75–0.90: below 0.75 the known-bad test fixture stops failing at all, and the reference palette stays clean across the whole range. 0.75 therefore sits at the lenient end, chosen so that a reported failure means something — a ratio tuned to maximise failures would deserve the scepticism it got. Both edges are pinned by tests.

Note the floors differ per condition. A single flat threshold would be wrong: Wong is weakest under tritanopia, which is also by far the rarest condition.

Input formats

Whatever shape you already have:

paletteguard palette.json                  # ["#e69f00", "#56b4e9"]
paletteguard palette.json                  # { "revenue": "#e69f00", ... }
paletteguard theme.css --prefix chart      # --chart-1: #e69f00;
echo '["#e69f00","#56b4e9"]' | paletteguard

Also accepted: [{ "name": "revenue", "color": "#e69f00" }], bare whitespace-separated hex, shorthand #abc, and a UTF-8 BOM.

Name your colours if you can. "revenue and expenses are indistinguishable" is actionable. "#e69f00 and #56b4e9 are indistinguishable" is a puzzle.

One palette, not a whole stylesheet

Give it the colours that identify series in a chart.

A stylesheet is not a categorical palette — --surface-raised and --surface-overlay are meant to look alike, and so are --accent and --accent-hover. Comparing everything against everything reports failures that aren't real. Narrow it with --prefix chart; paletteguard warns if you scan a whole file unfiltered.

Options

| Flag | | |---|---| | --prefix <s> | only CSS custom properties whose name contains <s> | | --json | machine-readable output | | --verbose | show passing pairs too | | --no-color | disable terminal colour | | --thresholds | print the calibrated floors and exit | | --help | usage |

Exit codes — 0 pass or warn · 1 at least one pair failed · 2 bad input

Continuous integration

- uses: actions/setup-node@v4
  with:
    node-version: 20
- run: npx paletteguard src/theme.css --prefix chart

Anything that reads an exit code works — GitLab CI, a pre-commit hook, a package.json script. Use --json if you want to post results somewhere.

As a library

import { checkPalette } from "paletteguard";

const report = checkPalette([
  { name: "revenue", hex: "#3ddc84" },
  { name: "expenses", hex: "#ff6961" },
]);

report.verdict;                  // "fail"
report.findings[0].distance;     // 8.96
report.findings[0].condition;    // "deuteranopia"
report.findings[0].seen;         // { a: "#a7a788", b: "#9b9b5d" }

Also exported: parsePalette, render, renderJson, seenAs, distance, referenceFloors, PASS_FLOOR, FAIL_FLOOR, WONG.

The check is importable on purpose. If a larger linter wants this capability, depending on this module should be the easiest path available.

How this compares

| | Checks CVD distinguishability | Runs in CI | Language | |---|:---:|:---:|---| | paletteguard | ✅ | ✅ | JS/TS | | colorblindcheck | ✅ | ❌ | R | | Viz Palette | ✅ | ❌ | web | | Color Oracle | simulates only | ❌ | desktop | | axe · Pa11y · Lighthouse | ❌ (contrast) | ✅ | JS | | color-contrast-checker etc. | ❌ (contrast) | ✅ | JS |

colorblindcheck is the closest prior work and is good — but being an R package means it cannot be installed into a JavaScript project or wired into a normal front-end pipeline. The interactive tools are genuinely useful, and they help only when someone remembers to open them.

What this does not do

  • It does not check WCAG contrast. A different failure mode, well covered by existing tools. This checks distinguishability, and staying out of contrast is the positioning rather than an oversight.
  • It does not claim WCAG compliance. WCAG 1.4.1 requires that colour is not the only carrier of meaning; its remedy is labels and patterns as much as colour choice. Fixing a palette is necessary, not sufficient.
  • It does not suggest replacement colours. Harder problem, and tools like Leonardo already generate palettes.
  • It does not handle sequential or diverging palettes. Different maths, different rules. Categorical only.

FAQ

Partly, yes — and it is the fairest objection. So ignore the thresholds and read raw minimum distances: Wong scores 12.19, the library defaults score 1.12–3.84. A 3–10× gap with no threshold involved. The audit works through this, plus the stronger version of the objection — that Wong is CVD-optimised and holding aesthetic palettes to it is a value judgement rather than a measurement.

Less. If series are labelled, patterned or separated by position, colour is no longer carrying the information alone — which is what WCAG 1.4.1 actually asks for. paletteguard measures the colours, not the whole chart. A failing pair on a directly-labelled chart is a smaller problem than the same pair in a legend.

It might not. Check what you actually use — pass only those three colours. The audit reports breaks at N for exactly this reason: schemeCategory10 fails at three, but schemeTableau10 survives to five.

Because it is far rarer — red-green deficiency is ~99% of cases — and because the reference palette is itself weakest there. Tritanopia failures are still reported, and marked rare so you can weigh them.

Not in 0.1.0. The floors are derived from a published reference on purpose, and a --threshold flag mostly invites people to tune until the build passes. If you have a real case for it, open an issue.

Contributing

See CONTRIBUTING.md. The scope is deliberately narrow and the boundaries are written down.

Prior art and credit

Licence

MIT © Rodvin Cosejo