CSS color has grown up beyond rgb() and hsl(). We now have Display P3, Oklab, OKLCH, relative colors, and color-mix(). These new tools are changing how designers and developers think about colors on the web.
Unfortunately, most JavaScript color libraries fall behind. Adding new syntax or spaces often takes months, leaving developers waiting for maintainers to catch up.
I wanted a different approach. Instead of shipping fixed utilities, I built Saturon: a runtime-extensible color engine that implements the entire <color> syntax as real JavaScript objects. That means you can:
- Parse any valid CSS color string, even nested or experimental syntax.
- Convert colors across all modern spaces with spec-accurate results.
- Extend the system with custom spaces, functions, and syntax in a few lines.
⚙️ A True Engine, Not Just a Converter
Saturon isn’t just a bag of utilities, it’s a full grammar-aware engine. It understands the entire CSS <color> syntax, including Level 4 and Level 5 constructs:
<color> = <color-base> | currentColor | <system-color> |
<contrast-color()> | <device-cmyk()> | <light-dark()>
<color-base> = <hex-color> | <color-function> | <named-color> |
<color-mix()> | transparent
<color-function> = <rgb()> | <rgba()> |
<hsl()> | <hsla()> | <hwb()> |
<lab()> | <lch()> | <oklab()> | <oklch()> |
<color()>
That means it can parse wild expressions like:
color-mix(
in oklch longer hue,
color(
from hsl(240deg none calc(-infinity) / 0.5)
display-p3
r calc(g + b) 100 / alpha
),
rebeccapurple 20%
)
You’ll never write CSS that wild, but Saturon can parse it anyway.
🖍️ Creating Colors
The Color class is the core API that represents any valid CSS color as a living object. Every color you work with in Saturon begins as a Color instance, which you can create in several ways depending on your needs.
🧩 1. Direct Model and Coordinates
If you already know the color model and component values, you can construct a color directly:
const color = new Color("rgb", [255, 0, 0]);
🧵 2. Parse from a CSS String
For everyday use, it’s easiest to create colors by parsing any valid CSS color string:
const color = Color.from("red");
🎲 3. Generate a Random Color
If you need a quick test data, you can generate random colors with:
const color = Color.random();
Each of these methods returns a fully functional Color instance that can be converted, manipulated, mixed, or fitted to a gamut, all using the same unified API.
🧪 Working with Colors
Once you’ve created a Color instance, Saturon gives you powerful ways to transform, convert, and combine it.
🔄 Converting Between Spaces
To create a new Color instance in another color space, use the .in() method:
const hsl = color.in("hsl");
This returns a new Color instance in the specified model. It’s perfect for manipulating colors in a specific space before manipulating (via .with()) or blending with another color (via .mix()).
🧾 Outputting Colors
You can serialize colors in several ways depending on your needs:
color.toString(); // → CSS color string
color.to("hex-color"); // → Hex string
color.toArray(); // → [r, g, b]
color.toObject(); // → { r, g, b }
🎨 Manipulating Components
The .with() method lets you adjust color components directly and returns a new Color instance, preserving immutability. Do you can chain transformations safely.:
color.with({ h: 120 }); // Set hue to 120°
color.with({ l: (l) => l / 2 }); // Modify lightness dynamically
color.with([120, 40, 10]); // Set all components at once
💫 Mixing Colors
Combine colors using CSS-accurate blending math with .mix(). It works just like CSS’s color-mix() function, with support for options such as amount, and hue interpolation.:
const red = Color.from("red");
const green = Color.from("green");
const mixed = red.in("lch").mix(green, { amount: 0.3, hue: "longer" });
console.log(mixed.to("rgb"));
Equivalent CSS:
color-mix(in lch longer hue, red, green 30%)
🌈 Gamut Mapping
When working across wide-gamut spaces, some colors can fall outside what a given device or color model can display. Saturon lets you decide exactly how those colors are handled.
🎯 The .within() Method
You can explicitly fit a color into a specific gamut using the .within() method. This creates a new Color instance that’s guaranteed to be inside the target gamut:
const fitted = color.within("srgb", "css-gamut-map");
The first argument defines the target gamut (e.g. "srgb", "display-p3", "rec2020"), and the second argument selects the mapping method:
-
"clip"→ clamps out-of-range values directly to the gamut edges -
"chroma-reduction"→ reduces chroma while preserving hue and lightness -
"css-gamut-map"→ applies the official CSS Color 5 mapping algorithm
Because .within() works independently of serialization, you can fit colors before further manipulation, ensuring predictable behavior across different spaces and outputs.
🧾 Gamut Mapping in Output
Gamut fitting can also be applied automatically when converting or serializing colors. All output methods, toString(), toArray(), and toObject(), support a { fit } option:
color.toString({ fit: "css-gamut-map" });
When used this way, the method applies gamut mapping according to the target gamut of the output space:
- For sRGB-bounded models like
rgbandhsl, colors are automatically fit to the srgb gamut. - For unbounded spaces like
lab,lch,oklab, oroklch, gamut mapping is skipped since these models have no display gamut limits.
This makes it easy to ensure your serialized colors remain display-safe without needing to manually constrain them first.
🔍 Utilities
The Color class also includes spec-based tools for comparison, contrast, and analysis:
| Method | Description |
|---|---|
deltaEOK(other) |
Perceptual ΔE in Oklab |
deltaE76(other) |
ΔE using CIE76 |
deltaE94(other) |
ΔE using CIE94 |
deltaE2000(other) |
ΔE using CIEDE2000 |
contrast(other) |
Contrast ratio (WCAG 2.1) |
equals(other) |
Equality tolerant of floating-point noise |
inGamut(space) |
Check if within a gamut (e.g. sRGB) |
🔌 Built for Extension
Saturon is extensible by design. You can register your own:
- Color functions (
jzazbz()) - Color spaces (
rec2100-pq) - Named colors (
duskmint) - Fit methods (
cam16-ucs) - Color bases (
wavelength()) - Even entirely new
<color>syntaxes (color-at())
Example: hsv() color function in a few lines:
registerColorFunction("hsv", {
bridge: "hsl",
components: {
h: { index: 0, value: "angle" },
s: { index: 1, value: "percentage" },
v: { index: 2, value: "percentage" },
},
toBridge: ([h, s, v]) => [
/* h, s, l */
],
fromBridge: ([h, s, l]) => [
/* h, s, v */
],
});
Then use it like any other CSS color:
Color.from("hsv(133grad none calc(infinity))").to("oklch");
Even inside complex expressions:
const mixed = Color.from(`
color-mix(
in hsv longer hue,
hsv(133grad none calc(infinity)) 30%,
blue
)
`);
It all just works.
🚀 Performance
Parsing the entire <color> grammar sounds heavy, but Saturon is highly optimized, and often faster than traditional object-oriented libraries.
10,000 parse + convert ops (Core i5-1135G7, Node 24):
| Test | saturon | culori | colorjs.io | chroma-js | color |
|---|---|---|---|---|---|
| Random input → random output | ~75ms | ~25ms | ~230ms | ~250ms | ~110ms |
Between color() spaces |
~95ms | ~240ms | ~230ms | ❌ | ❌ |
| Relative color → random output | ~230ms | ❌ | ❌ | ❌ | ❌ |
color-mix() → random output |
~380ms | ❌ | ❌ | ❌ | ❌ |
⚖️ Spec Compliance & Tests
Saturon passes the full Web Platform Tests for CSS Color validation, with only a few exceptions (sign() with font-relative units).
You can explore the test suites directly. They contain dozens of detailed cases that demonstrate how Saturon aligns with the specs.
🔗 Try It
Saturon is open source and written in TypeScript. It’s still young, so feedback and bug reports are very welcome!
👉 Docs: https://saturon.js.org
👉 npm: https://www.npmjs.com/package/saturon
👉 GitHub: https://github.com/ganemedelabs/saturon
Top comments (0)