Skip to content

Repository files navigation

@colordx/cli

npm version CI MIT license

Check, audit, and fix color contrast in your design tokens from the command line. Built on @colordx/core. WCAG 2.2 and APCA gates.

Ships with a Claude Code skill. The CLI does the math. The model follows the rules.

Install

The CLI:

npm install -D @colordx/cli

The skill, in Claude Code, with the hook that audits every token edit:

claude plugin marketplace add dkryaklin/colordx-a11y
claude plugin install colordx-a11y@colordx

The skill in Codex, Cursor, or any agent that reads SKILL.md. There is no hook there, so run the CI check:

npx skills add dkryaklin/colordx-a11y

Quick start

npx @colordx/cli check '#777' '#fff'                     # one pair, body text, AA
npx @colordx/cli check '#fff' 'rgb(0 0 0 / .5)' '#fff'   # a stack, top first
npx @colordx/cli info '#ffdd00'                          # one color: oklch, rgb, hsl, p3, contrast on white and on black
npx @colordx/cli audit src/globals.css                   # every known pair, both modes
npx @colordx/cli scale '#3b82f6' --anchor 600:#fff       # ramp, step 600 passes on white
npx @colordx/cli ladder blue=#1d70b8 red=#d4351c         # one ramp per seed, on shared steps
npx @colordx/cli theme --primary '#3b82f6'               # shadcn roles, light and dark

The bin is colordx. With the package installed, npx colordx and a colordx script both work.

Every command prints JSON. Add --format table for a human view, painted with the real colors in a terminal, or --format html for a preview page with every pair as real text on its real bg, as is and with each fix. For scale, ladder and theme, the page lays the values out as a grid, one row per group and one column per step, each cell painted, with its contrast on white and on black. Exit code is 1 on any fail, 2 on bad input, a bad config, or nothing to check. --strict fails on warn and unpaired too.

Words

Word Meaning
pair one fg on one bg
stack fg, bg, and every bg behind it, down to the page
use what the pair is for: body, large, ui
level AA or AAA
gate the check a pair must pass: wcag, apca, both
result pass, fail, warn, unpaired, skip
fix the nearest color that passes
preset a named token convention that pairs colors by name

The full list is in spec.md. Every number is in thresholds.md.

Output

One record per pair:

{
  "fg": "#777777",
  "bg": "#ffffff",
  "fgToken": "--muted-foreground",
  "bgToken": "--muted",
  "use": "body",
  "level": "AA",
  "mode": "light",
  "wcag": { "ratio": 4.47, "min": 4.5, "pass": false },
  "apca": { "lc": 71.1, "min": 75, "pass": false },
  "result": "fail",
  "fix": "#777677",
  "fixBg": "#fdfdfd",
  "pairedBy": "shadcn",
  "source": { "file": "src/globals.css", "line": 17 },
  "note": null
}
  • wcag is the default gate. APCA still runs. WCAG pass with APCA fail is warn.
  • The check runs on the color as shown: the stack flattened, the gamut mapped, sRGB quantised to 8 bits.
  • The gate runs on the full value. The shown ratio and lc are cut toward zero, not rounded, so a shown number at or past min always means the pair passed. 4.4996:1 fails and shows as 4.49.
  • fix keeps hue, moves lightness, drops chroma only when it must. It passes the gate as written, in sRGB and in P3: the string is checked again the way a re-run would check it. It clears the APCA warn too, so a re-check comes back clean. When no color with the hue can, the fix meets the gate alone and note says so. Null when nothing with that hue passes.
  • One token that fails on several bgs gets one fix, the one that passes on all of them, on every row. note says so.
  • A fix far from the color it replaces changes the look. note says fix is a visible change. The skill shows the pair and asks before it edits. The distance is in thresholds.md.
  • fixBg is the bg moved instead, when the bg is one color and can stay on its side of the fg. Null for a stack, or when only a flip would pass.
  • source is where the fg is declared. A literal is declared where it is used. Edit there.

scale, ladder and theme add values: the ramp, the steps per seed, or the tokens per mode. info adds colors, one per color, and has no records.

Commands

check

colordx check <fg> <bg...> [--use body|large|ui] [--level AA|AAA] [--gate wcag|apca|both] [--gamut srgb|p3] [--mode light|dark]

More than one bg is a stack, top first. The record keeps the stack in bgToken and the flattened color in bg. An alpha at the bottom is unpaired. A value that is not a color is exit 2. --mode only tags the record.

audit

colordx audit [files...] [--preset shadcn|material|tailwind]

Reads tokens from CSS custom properties, Tailwind v4 @theme, tailwind.config.*, DTCG JSON, and SCSS variables. Each token keeps its file and line. var(), $x and {a.b} references are filled in. A bare h s% l% triplet is read as hsl().

Modes come from the scope, the selectors and at-rules around a token. A scope with a dark pattern like .dark, [data-theme="dark"], or prefers-color-scheme: dark is dark. The last pattern found wins, so .dark .light is light. Anything else is light. Dark starts from light. Each mode runs the full gate. A token declared only in a dark scope is still checked, and reported when it has no pair. @media (color-gamut: p3) overrides apply under --gamut p3 and are ignored otherwise.

Presets pair colors and say so in pairedBy. shadcn, material and css are on by default. tailwind scans markup, so add it with --preset tailwind or in the config:

Preset Pairs
shadcn --x-foreground on --x. --border, --input, --ring are ui on --background.
material --md-sys-color-on-x on --md-sys-color-x. outline is ui on surface.
css In a CSS or SCSS rule that sets both: color is body and a border is ui on the rule's background. The scope sets the mode.
tailwind text-*, border-*, ring-*, outline-* with the bg-* on the element or above, in markup. dark: sets the mode.

The css preset reads nothing across rules. A rule with no bg gives no pair, since what is behind it is not in the file. A rule with a translucent bg is unpaired until a config pair says what is behind it. An outline paints on the parent, so it is not paired. A side that is not a color, like a gradient, gives no pair.

A color with no known pair is unpaired. Nothing is skipped in silence. A glob that matches nothing, or a run with no tokens and no pairs, stops with exit 2.

status in the config is a set of colors that must stay apart, like success, warning, and error. Every pair in it is simulated for protanopia, deuteranopia, and tritanopia. A pair that is distinct to normal vision and not under any one type is a warn, never a fail. The record has the two colors in fg and bg, the type and where the second color is declared in note, and null in use, level, wcag and apca.

info

colordx info <color...>

Reads each color: hex, rgb, hsl, oklch as l, c and h, p3, whether it is in sRGB, alpha, luminance, and its contrast on white and on black in white and black, as ratio and lc. The sRGB forms of a color outside sRGB are its map. No pair is checked, so the exit code is 0.

scale

colordx scale <seed> [--anchor <step>:<bg>[:<use>]]...

An OKLCH ramp, 50 to 950, one hue. The seed sits at the step nearest its lightness, kept as given, and the other steps reshape around it. An anchor is a pair: the step as fg on a color, or on another step. A failing anchor moves its step to the fix, and the ramp reshapes around it. Generation solves for both gates and reports under yours. An anchor that pushes its step past a neighbour is a warn, and note says which step to anchor instead.

ladder

colordx ladder <seed...> [--step <name>[:<L>]]... [--anchor <step>:<bg>[:<use>]]...

One ramp per seed, on shared steps. A seed is a color, or <name>=<color>. The name is the key in values and the prefix of each step's token, like blue-600. Steps go light to dark, each with its OKLCH lightness, 0.55 or 55%. A scale step like 600 can leave it out and take the scale's. Default is the scale's 50 to 950. Each seed keeps its hue, sits at the step nearest its lightness, and is kept as given. Chroma follows the scale's shape at each step's lightness, scaled to the seed. An anchor runs on every seed, and a step as bg is a step of the same ramp. Steps named with numbers sort first in the JSON, ascending, as object keys do.

theme

colordx theme --primary <color> [--neutral <color>] [--destructive <color>]

Solves the shadcn roles for light and dark. Each -foreground is moved until it passes both gates on its bg. When no fg can pass, the role moves instead and note says so. Dark is solved, not inverted. One record per role and mode.

Config

colordx.config.json, next to where you run. Every command reads it. Flags win.

{
  "$schema": "node_modules/@colordx/cli/colordx.schema.json",
  "gate": "wcag",
  "level": "AA",
  "gamut": "srgb",
  "files": ["src/**/*.css", "src/**/*.tsx"],
  "presets": ["shadcn", "tailwind"],
  "pairs": [{ "fg": "--muted-foreground", "bg": ["--card", "--background"], "level": "AAA" }],
  "skips": [{ "token": "--input", "reason": "decorative" }],
  "status": ["--color-green-600", "--color-red-600", "--color-amber-500"]
}
  • pairs: declared pairs. bg is one token or the stack, top first. A side is a token, a token with an alpha in percent like --gray-12/60, or a color. Add use, level, or mode per pair.
  • skips: a token, or a literal color as written in the file, checked on purpose and left out. Reasons: disabled, decorative, logotype.
  • status: colors that must stay apart under CVD. Token names or colors.
  • modes: scope patterns per mode, if the defaults do not match yours. Your list replaces the default list for that mode.

The file is checked against the schema. A bad value or an unknown key stops the run with exit 2 and says which key.

CI

Exit code 1 on any fail, and with --strict on warn and unpaired too. A workflow is one step, see examples/ci.yml:

- run: npx @colordx/cli audit --strict

Files, presets, pairs, and skips come from colordx.config.json.

Skill

skills/colordx-a11y is a Claude Code skill. The plugin manifest is in .claude-plugin. The rules the model follows are in SKILL.md, and nothing else is.

Hook

The plugin ships one PostToolUse hook. After the model edits a CSS, SCSS, DTCG JSON, or tailwind.config.* file, the hook runs audit --format table and hands the fail and unpaired rows back to the model, with a count of the warn rows. When the CLI does not run, the hook says so instead of staying quiet. Without it the skill is advice. With it every token edit is checked.

Without the plugin, the same hook goes in .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [{ "type": "command", "command": "node node_modules/@colordx/cli/hooks/audit.mjs" }]
      }
    ]
  }
}

Evals

evals/ has three cases: find the failing pair in a file, generate a dark theme, refuse to guess an unpaired color. Run them with claude plugin eval ..

Thresholds

thresholds.md is the only file with numbers. The CLI ships thresholds.json. A test parses the markdown and asserts the two match, and that the installed apca-w3 is the pinned version.

Out of scope

Gradients, images, motion, runtime layers. Every report says so.

License

MIT

About

Check, audit, and fix color contrast in your design tokens from the command line.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages