npm.io
1.2.0 • Published 1 week ago

csscolorparser-ts

Licence
MIT
Version
1.2.0
Deps
0
Size
162 kB
Vulns
0
Weekly
0
Stars
1

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, and null otherwise.
  • Every CSS Color 4 color function is supported, but only static values. oklch(var(--theme-lightness) 0.102 42) might be a valid color; resolving var() 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] or null. Nothing else, ever — no exceptions thrown, no undefined.
  • r, g and b are rounded integers in 0–255. a is 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:

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).

Keywords