csscolorparser-ts
Parser for CSS color strings written in TypeScript. Zero runtime dependencies.
parseCSSColor('oklch(0.7 0.1 200)');
// [ 64, 177, 183, 1 ]
TL;DR
- Exports a single function,
parseCSSColor(string), returning[R, G, B, A]—[0-255, 0-255, 0-255, 0.0-1.0]— for a color it can resolve, andnullotherwise. - Every CSS Color 4 color function is supported, but only static values.
oklch(var(--theme-lightness) 0.102 42)might be a valid color; resolvingvar()needs a stylesheet, which is outside the scope of this library. - The returned RGB is sRGB. Color spaces wider than sRGB are converted with loss, and the way that loss is applied matches browser behavior as closely as possible.
- It aims to be fast: a single pass over the string, with no allocation beyond the array it hands back — tens of millions of parses per second on ordinary laptop hardware.
Install
const { parseCSSColor } = require('csscolorparser-ts'); // CommonJS
import { parseCSSColor } from 'csscolorparser-ts'; // ESM
TypeScript users also get the RGBA type, short for [number, number, number, number].
Supported syntax
parseCSSColor('tomato'); // [ 255, 99, 71, 1 ]
parseCSSColor('#AABBCC'); // [ 170, 187, 204, 1 ]
parseCSSColor('#ABC'); // [ 170, 187, 204, 1 ]
parseCSSColor('#AABBCCCC'); // [ 170, 187, 204, 0.8 ]
parseCSSColor('#ABCC'); // [ 170, 187, 204, 0.8 ]
parseCSSColor('rgba(255, 128, 0, 0.5)'); // [ 255, 128, 0, 0.5 ]
parseCSSColor('rgb(255 128 0 / 0.5)'); // [ 255, 128, 0, 0.5 ]
parseCSSColor('hsla(900, 15%, 90%, 0.5)'); // [ 226, 233, 233, 0.5 ]
parseCSSColor('hsla(2rad, 15%, 90%, 0.5)'); // [ 226, 233, 226, 0.5 ]
parseCSSColor('hwb(194 30% 40% / .5)'); // [ 77, 135, 153, 0.5 ]
parseCSSColor('lab(50% 40 59.5)'); // [ 191, 87, 0, 1 ]
parseCSSColor('lch(50% 70 40)'); // [ 206, 71, 45, 1 ]
parseCSSColor('oklab(0.5 -0.1 0.1)'); // [ 60, 116, 10, 1 ]
parseCSSColor('oklch(70% 0.1 200 / 50%)'); // [ 64, 177, 183, 0.5 ]
// color() accepts srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb,
// rec2020, xyz, xyz-d50 and xyz-d65
parseCSSColor('color(srgb 0.5 0.2 0.7)'); // [ 128, 51, 179, 1 ]
parseCSSColor('color(display-p3 1 0 0)'); // [ 255, 0, 0, 1 ]
parseCSSColor('color(rec2020 0.9 0.1 0.1)'); // [ 255, 0, 22, 1 ]
parseCSSColor('color(xyz-d50 0.2 0.3 0.4)'); // [ 0, 168, 189, 1 ]
parseCSSColor('color(srgb-linear 0.5 0.5 0.5)'); // [ 188, 188, 188, 1 ]
// the none keyword, bare numbers for saturation and lightness, comments
// inside values, and every angle unit
parseCSSColor('rgb(none 128 0)'); // [ 0, 128, 0, 1 ]
parseCSSColor('hsl(50 80 35)'); // [ 161, 137, 18, 1 ]
parseCSSColor('rgb(1 /*a comment*/ 2 3)'); // [ 1, 2, 3, 1 ]
parseCSSColor('\trgb(50 \t 100 \n 200 / 255)\t');// [ 50, 100, 200, 1 ]
// malformed
parseCSSColor('nosuchcolor'); // null
parseCSSColor('ffffff'); // null
parseCSSColor('rgb(50, 50)'); // null
parseCSSColor('rgb(50, 50a, 50)'); // null
parseCSSColor('hsl(900, 0.15, 90%)'); // null legacy hsl() needs percentages
parseCSSColor('rgb(5%, 50, 30)'); // null legacy rgb() may not mix types
parseCSSColor('rgb(132 170 73 0.5)'); // null alpha needs a slash
What null means
null means this library could not produce an sRGB value. That covers strings that are not colors at all, and also values a browser accepts but whose color depends on context this library does not have — there is no cascade, no element, no color scheme and no OS theme to consult:
parseCSSColor('rgb(var(--brand), 1, 2)'); // null — may be a color, needs a stylesheet
parseCSSColor('currentcolor'); // null — needs an element
parseCSSColor('Canvas'); // null — needs an OS theme
parseCSSColor('light-dark(red, blue)'); // null — needs the color scheme
parseCSSColor('inherit'); // null — needs the cascade, as do initial and unset
parseCSSColor('color(--my-profile 0 0 0)'); // null — needs an @color-profile rule
So !parseCSSColor(x) answers "can I get an sRGB value out of this?", which is usually the question you want — but it is not a general "is this valid CSS?" test, because every browser accepts all of the above.
Static, but not implemented yet
parseCSSColor('color-mix(in oklab, red, blue)'); // null, but resolves to [ 140, 83, 162, 1 ]
parseCSSColor('rgb(from red r g b)'); // null, but resolves to [ 255, 0, 0, 1 ]
parseCSSColor('rgb(calc(10 + 20) 30 40)'); // null, but resolves to [ 30, 30, 40, 1 ]
Stability contract
Everything below is about parseCSSColor specifically, and will not change for it without a major version. Anything with different semantics would arrive as its own function rather than as a change to this one.
- The result is
[r, g, b, a]ornull. Nothing else, ever — no exceptions thrown, noundefined. r,gandbare rounded integers in 0–255.ais a float in 0–1.- The array always has exactly four elements, so
new Color(...parsed)is safe. - The array is freshly allocated on every call, so you may mutate it freely.
- Reconstructing a color string from the result is supported:
`rgba(${r},${g},${b},${a})`will parse back to the same values.
Scope
The conversion is lossy, on purpose
Every color function outside sRGB — lab(), oklch(), color(display-p3 …) and the rest — can name colors your screen cannot show. Converting those to an RGBA byte triple therefore loses information, irreversibly:
parseCSSColor('oklch(0.7 0.25 200)'); // [ 0, 198, 219, 1 ]
parseCSSColor('oklch(0.7 0.4 200)'); // [ 0, 212, 254, 1 ]
Those are visibly different colors that land two steps apart. In sRGB coordinates the second is (-0.82, 0.83, 1.00) — the negative red means "you would have to remove red light", which no display can do. Reducing such a color to something displayable is called gamut mapping, and this library does it the way browsers do: per-channel clipping.
Why clipping, when the spec says otherwise
CSS Color 4 §11 requires that a color destined for a display "must be css gamut mapped", and §14.2 permits three algorithms, none of which is clipping — §14.1.1 calls clipping "the simplest and least acceptable method".
Every engine clips anyway. This library matches them, so the RGBA you get is the color that would actually be painted, and drawing it on a canvas beside a DOM element with the same declared color produces no visible seam. That is a deliberate choice to match reality over the specification, and the upstream issues are still open:
- csswg-drafts#9449 — the umbrella issue
- WebKit 255939, Mozilla 1847421, crbug 1440069
- browser-compat-data#26838 — "not spec compliant"
If an engine ships real gamut mapping, this default will be revisited.
Correctness
The test harness compares this library against real Chromium, Firefox and WebKit, by painting swatches and sampling the rendered pixels. The fixtures are committed, so running the tests needs no browser.
The engines disagree with each other in small ways, and each is out of spec somewhere the others are not, so no single one is authoritative: where they differ, this library follows the specification. Each engine's known deviations are listed explicitly in the test suite, with the tolerance or the reason.
To regenerate the fixtures (requires Docker):
npm run reference:generate
Credits
- Dean McNamee, author of the original csscolorparser library (GitHub, NPM) which this library is forked from.
- adroitwhiz, for excellent testcases for CSS color parsers (GitHub).