Convert colors and measure how readable they are.
Retna reads a color in almost any notation, normalizes it to sRGB, and then
either prints it in another color space or measures it against another color.
Contrast comes from WCAG 2.x by default, and APCA is available behind
--algorithm.
It writes nothing to disk and keeps no configuration file. Every command reads its arguments and prints to stdout. There is an agent skill that teaches a coding agent to drive it.
On macOS, with Homebrew:
brew install --cask d3uceY/retna/retna
Prebuilt binaries for Linux, macOS and Windows are on the
releases page. Unpack the archive
and put retna somewhere on your PATH.
With Go 1.25 or newer:
go install github.com/d3uceY/Retna@latest
Or build from a clone:
git clone https://github.com/d3uceY/Retna
cd Retna
go build -o retna .
retna contrast "#777" "#fff"
Contrast
Foreground #777777
Background #FFFFFF
WCAG
Ratio 4.48:1
AA Normal FAIL
AA Large PASS
AAA Normal FAIL
AAA Large FAIL
Give it a foreground and a background. Add --against to check one foreground
against a list instead:
retna contrast "#777" --against "#fff,#000,#f5f5f5,#101010"
Contrast
Foreground #777777
Color Ratio AA AAA
────────── ────── ──── ────
#FFFFFF 4.48:1 FAIL FAIL
#000000 4.69:1 PASS FAIL
#F5F5F5 4.11:1 FAIL FAIL
#101010 4.25:1 FAIL FAIL
--against can be repeated, and each value may hold a comma separated list:
retna contrast "#777" --against "#fff" --against "#000,#101010"
Add --all to run every algorithm and print each one in turn:
retna contrast red white --all
--min takes a ratio. If any pair falls short, Retna prints the verdict and
exits 1.
retna contrast "$FG" "$BG" --min 4.5
FAIL 4.48:1
Required: 4.50:1
# .github/workflows/a11y.yml
- name: Check color contrast
run: retna contrast "${{ vars.FG }}" "${{ vars.BG }}" --min 4.5With --against, the summary line reports how many pairs failed. The exit code
is 1 when at least one did.
--min compares the WCAG ratio, so it needs the wcag algorithm to be selected.
Combining it with --algorithm apca is an error rather than a check that quietly
finds nothing to measure. --all includes wcag, so it works there. The value
has to be a real number above zero, since --min 0 would pass anything.
--to takes one space, a comma separated list, or all.
retna convert "#1a73e8" --to hsl,oklch
#1A73E8
HSL hsl(214.08, 81.75%, 50.59%)
OKLCH oklch(0.5737, 0.1946, 257.86)
With no --to, every space is printed.
inspect prints every space plus the relative luminance.
retna inspect "#3498db"
Color
HEX #3498DB
RGB rgb(52, 152, 219)
RGBA rgba(52, 152, 219, 1)
HSL hsl(204.07, 69.87%, 53.14%)
HSV hsv(204.07, 76.26%, 85.88%)
HWB hwb(204.07 20.39% 14.12%)
LAB lab(59.5, -12.1, -43.14)
LCH lch(59.5, 44.8, 254.33)
OKLAB oklab(0.6531, -0.0618, -0.1197)
OKLCH oklch(0.6531, 0.1347, 242.69)
SRGB color(srgb 0.2039 0.5961 0.8588)
DISPLAY-P3 color(display-p3 0.3208 0.588 0.8369)
A98-RGB color(a98-rgb 0.3725 0.5905 0.8459)
PROPHOTO-RGB color(prophoto-rgb 0.4327 0.51 0.7876)
REC2020 color(rec2020 0.3766 0.5394 0.8141)
Relative luminance
Luminance 0.2830
This does the same measurements as contrast --against, but the colors come
from a file or from stdin. It saves typing when the list already exists
somewhere else.
retna palette contrast --foreground white --file palette.txt
retna palette contrast --foreground white --file - < palette.txt
retna palette contrast --foreground white --colors "#000,#111,#666"
The file holds one color per line, and each line may also be a comma separated
list. Blank lines and lines starting with // are skipped, and a UTF-8 byte
order mark at the start of the file is ignored, so a file saved by a Windows
editor works as-is.
Contrast
Foreground #FFFFFF
Color Ratio AA AAA
────────── ─────── ──── ────
#000000 21.00:1 PASS PASS
#111111 18.88:1 PASS PASS
#666666 5.74:1 PASS FAIL
readable answers the other direction. Given a background, which text color
reads best on it?
retna readable "#3498db"
Background #3498DB
Text color Ratio AA
────────── ────── ────
#000000 6.66:1 PASS
#FFFFFF 3.15:1 FAIL
Candidates default to black and white. Use --candidates to try your own,
--level AAA to raise the bar, or --best to print only the winner.
text-color works as an alias for the command.
fix nudges a color until it passes. It walks OKLab lightness in the direction
that increases contrast and leaves chroma and hue alone, so the result still
looks like the color you started with.
retna fix "#777777" --background "#ffffff" --level AA
Fix
Original #777777 4.48:1 FAIL
Background #FFFFFF
Target 4.50:1
Suggested colors
Color Ratio WCAG Note
────────── ────── ──── ───────────────
#767676 4.52:1 PASS smallest change
#757575 4.59:1 PASS
#747474 4.65:1 PASS
Use --target 7 for an explicit ratio, or --suggest to control how many
alternatives are listed. A color that already passes is reported and left
alone.
The target has to sit between 1:1 and 21:1. Black on white is the highest ratio
there is, so anything above 21 cannot be reached and is rejected instead of
reported as unfixable. --suggest needs at least 1.
The suggested colors are measured after rounding to 8-bit channels, so the hex shown is the color that actually meets the ratio on that line.
Hex works with or without the hash, in three, four, six or eight digit form:
#fff #ffff #ffffff #ffffff80
Functional notation accepts commas, spaces, or a slash before the alpha value:
rgb(26, 115, 232) rgb(26 115 232) rgb(26 115 232 / 0.5)
hsl(214.08 81.75% 50.59%) hsla(214.08, 81.75%, 50.59%, 0.5)
hsv(214.08 88.79% 90.98%) hwb(214.08 10.2% 9.02%)
lab(48.77 9.69 -67.47) lch(48.77 68.16 278.18)
oklab(0.5737 -0.0409 -0.1902) oklch(0.5737 0.1946 257.86)
Angles accept deg, rad, grad or turn, and percentages work anywhere CSS
allows them. The CSS color names are matched case insensitively, so
rebeccapurple, RebeccaPurple and #663399 all name the same color.
transparent is accepted too.
The wide gamut spaces arrive through CSS color(), which names its space
first:
color(srgb 0.2039 0.5961 0.8588) color(display-p3 0.3208 0.588 0.8369)
color(a98-rgb 0.3725 0.5905 0.8459) color(prophoto-rgb 0.4327 0.51 0.7876)
color(rec2020 0.3766 0.5394 0.8141) color(display-p3 1 0 0 / 0.5)
Components take numbers or percentages and are allowed to fall outside 0..1, which is how CSS names colors the sRGB gamut cannot hold. Retna keeps those values rather than treating them as mistakes, because a negative component still names a real color. Clipping only happens when the color has to be drawn as sRGB.
hex, rgb, rgba, hsl, hsv, hwb, lab, lch, oklab, oklch,
srgb, display-p3, a98-rgb, prophoto-rgb, rec2020
Lab, LCH and ProPhoto use the D50 white point, which is what CSS specifies. OKLab, OKLCH, display-p3, a98-rgb and rec2020 use D65.
Turning an sRGB color into one of the wider spaces cannot land outside their
gamuts, so those components are reported without clipping. A clipped component
would name a different color. Going the other way, a color() value outside
the sRGB gamut is clipped to the nearest displayable color.
--json works on every command. For contrast:
retna contrast "#777" "#fff" --json
{
"foreground": "#777777",
"background": "#FFFFFF",
"contrast": 4.478089453577214,
"wcag": {
"aa": { "normal": false, "large": true },
"aaa": { "normal": false, "large": false }
}
}With --against, the measurements become a list under results. Selecting
--algorithm apca adds an apca field holding Lc.
inspect --json returns each space as a three number group plus luminance:
{
"input": "#3498db",
"hex": "#3498DB",
"alpha": 1,
"rgb": [52, 152, 219],
"hsl": [204.0719, 0.6987, 0.5314],
"hsv": [204.0719, 0.7626, 0.8588],
"hwb": [204.0719, 0.2039, 0.1412],
"lab": [59.4961, -12.1034, -43.1352],
"lch": [59.4961, 44.8011, 254.3263],
"oklab": [0.6531, -0.0618, -0.1197],
"oklch": [0.6531, 0.1347, 242.6867],
"srgb": [0.2039, 0.5961, 0.8588],
"display-p3": [0.3208, 0.588, 0.8369],
"a98-rgb": [0.3725, 0.5905, 0.8459],
"prophoto-rgb": [0.4327, 0.51, 0.7876],
"rec2020": [0.3766, 0.5394, 0.8141],
"luminance": 0.283
}Each group is one line here for readability; the command itself indents every number onto its own line.
Hues are degrees, saturation style channels run from 0 to 1, Lab lightness runs from 0 to 100, and OKLab lightness runs from 0 to 1. The wide gamut components are raw, so they can sit outside 0..1.
WCAG and APCA both measure opaque colors. When either input has alpha, Retna composites the background over white and then the foreground over that background, which is what a browser would paint, and prints a note so the substitution is not silent.
--algorithm apca returns Lc. The sign carries meaning: dark text on a light
background is positive, and light text on a dark background is negative, so
compare absolute values. The built in checks use the 45, 60 and 75 bands.
skill/SKILL.md is an agent skill for building and auditing UI
color systems with Retna as the measuring instrument: pick a ramp, then check
every text and surface pair instead of estimating a ratio. It carries reference
docs on the CLI, palette recipes, dark mode, accessibility thresholds and color
science.
Copy the folder into your agent's skills directory:
cp -r skill ~/.copilot/skills/retna-color-expert # VS Code / Copilot
cp -r skill ~/.claude/skills/retna-color-expert # Claude Code
It is plain markdown and nothing is fetched at runtime, so a clone already has
everything: read skill/SKILL.md from the checkout, or start
with skill/references/retna-cli.md for the
command reference. Installing with go install, Homebrew or a release archive
gives you the binary and nothing else, so clone the repo to get skill/.
skill/references/color-expert/ is 2.4 MB of the folder's 2.5 MB. It is a copy
of meodai/skill.color-expert and
keeps its own license, CC BY 4.0 with third-party material credited per file,
while the rest of the skill is MIT, like Retna. Details in
skill/ATTRIBUTION.md. Dropping that one subfolder leaves
an 88 KB skill that still works, minus its links into the deep references.
MIT. See LICENSE.
