Frost Color is a dependency-free JavaScript library for parsing, converting, inspecting, and formatting colors. It supports modern CSS color spaces as well as linear-light working spaces, preserves out-of-gamut channel values during conversion, and provides explicit gamut fitting when a bounded output is required.
- Default ESM
Colorexport and named concrete-class exports for Node and bundlers - Prebuilt ESM and UMD bundles in
dist/, with UMD exposed asglobalThis.Color - No runtime dependencies
- Immutable color instances and transformations
- Modern CSS color parsing, formatting, and named colors
- Conversion across bounded, wide-gamut, perceptual, and linear-light spaces
- Explicit gamut fitting without clipping intermediate conversions
npm i @fr0st/colorFrost Color's package entry point is ESM-only. Use import syntax in Node and bundlers.
import Color, { DisplayP3 } from '@fr0st/color';Import the minified ESM bundle directly from a CDN:
<script type="module">
import Color, { DisplayP3 } from 'https://cdn.jsdelivr.net/npm/@fr0st/color@latest/dist/frost-color.esm.min.js';
const color = Color.fromString('lavender');
const displayP3 = DisplayP3.fromString('lavender');
</script>Load the bundle from your own copy or a CDN:
<script src="/path/to/dist/frost-color.min.js"></script>
<!-- or -->
<script src="https://cdn.jsdelivr.net/npm/@fr0st/color@latest/dist/frost-color.min.js"></script>
<script>
const color = globalThis.Color.fromString('oklch(0.7 0.15 250)');
</script>The package root resolves to the prebuilt ESM bundle. Bundles under dist/ and the source entry src/index.js are also available through matching package subpaths.
import Color from '@fr0st/color';
const source = Color.fromString('color(display-p3 1 0.2 0.1 / 80%)');
const srgb = source.fitGamut('srgb').toSrgb();
const translucent = srgb.withAlpha(0.5);
console.log(source.space()); // display-p3
console.log(srgb.toString()); // color(srgb 1 0.28 0.2 / 0.8)
console.log(translucent.getAlpha()); // 0.5
console.log(source.getAlpha()); // 0.8; source was not changedColor instances are immutable and frozen. Conversion and with... methods do not mutate their source; they return either a new instance or, when no conversion is needed, the same instance. Channel fields remain readable, but use the provided getters and with... methods instead of assigning them.
Color.fromString(value) accepts the following forms:
- CSS named colors and
transparent - Hex colors:
#rgb,#rgba,#rrggbb, and#rrggbbaa rgb()and legacyrgba()hsl()and legacyhsla()hwb(),lab(),lch(),oklab(), andoklch()color()usinga98-rgb,display-p3,display-p3-linear,prophoto-rgb,rec2020,srgb,srgb-linear,xyz,xyz-d50, orxyz-d65
Whitespace is normalized and parsing is case-insensitive. Functional notation requires exactly three channels and accepts one optional alpha value. Space-separated alpha uses /; legacy comma syntax is supported for RGB, HSL, and HWB, but separator styles cannot be mixed. Numeric tokens support signs, decimals, percentages, and scientific notation, and must not contain trailing characters or unknown units.
const hex = Color.fromString('#663399cc');
const hsl = Color.fromString('hsl(270deg 50% 40% / 80%)');
const p3 = Color.fromString('color(display-p3 90% 20% 10%)');
const lab = Color.fromString('lab(60% 30 -20)');This is a focused color-value parser, not a complete CSS value engine. CSS-wide keywords, currentcolor, none components, var(), calc(), relative colors, and custom color profiles are outside its scope.
Concrete classes are available both as named exports and as static properties on the default export. Each numeric from... factory accepts three channels, each defaulting to 0, followed by optional alpha = 1.
| Factory or constructor | Concrete class | space() identifier |
Channels | Native toString() form |
|---|---|---|---|---|
fromA98Rgb(r, g, b, alpha) |
Color.A98Rgb |
a98-rgb |
normalized red, green, blue | color(a98-rgb ...) |
fromDisplayP3(r, g, b, alpha) |
Color.DisplayP3 |
display-p3 |
normalized red, green, blue | color(display-p3 ...) |
fromDisplayP3Linear(r, g, b, alpha) |
Color.DisplayP3Linear |
display-p3-linear |
linear red, green, blue | color(display-p3-linear ...) |
new Color.Hex(r, g, b, alpha) or fromString(...) |
Color.Hex |
hex |
red, green, blue on a 0–255 scale | #rgb, #rgba, #rrggbb, or #rrggbbaa |
fromHsl(h, s, l, alpha) |
Color.Hsl |
hsl |
hue in degrees, saturation and lightness in percent | hsl(...) |
fromHwb(h, w, b, alpha) |
Color.Hwb |
hwb |
hue in degrees, whiteness and blackness in percent | hwb(...) |
fromLab(l, a, b, alpha) |
Color.Lab |
lab |
lightness on a 0–100 scale, a, b | lab(...) |
fromLch(l, c, h, alpha) |
Color.Lch |
lch |
lightness on a 0–100 scale, chroma, hue in degrees | lch(...) |
fromOkLab(l, a, b, alpha) |
Color.OkLab |
oklab |
lightness on a 0–1 scale, a, b | oklab(...) |
fromOkLch(l, c, h, alpha) |
Color.OkLch |
oklch |
lightness on a 0–1 scale, chroma, hue in degrees | oklch(...) |
fromProPhotoRgb(r, g, b, alpha) |
Color.ProPhotoRgb |
prophoto-rgb |
normalized red, green, blue | color(prophoto-rgb ...) |
fromRec2020(r, g, b, alpha) |
Color.Rec2020 |
rec2020 |
normalized red, green, blue | color(rec2020 ...) |
fromRgb(r, g, b, alpha) |
Color.Rgb |
rgb |
red, green, blue on a 0–255 scale | rgb(...) |
fromSrgb(r, g, b, alpha) |
Color.Srgb |
srgb |
normalized red, green, blue | color(srgb ...) |
fromSrgbLinear(r, g, b, alpha) |
Color.SrgbLinear |
srgb-linear |
linear red, green, blue | color(srgb-linear ...) |
fromXyzD50(x, y, z, alpha) |
Color.XyzD50 |
xyz-d50 |
D50-relative x, y, z | color(xyz-d50 ...) |
fromXyzD65(x, y, z, alpha) |
Color.XyzD65 |
xyz-d65 |
D65-relative x, y, z | color(xyz-d65 ...) |
Factories on Color return the corresponding source-space class. The same inherited factories can construct directly in a chosen destination space:
const okLch = Color.fromOkLch(0.7, 0.15, 250);
const lab = Color.Lab.fromOkLch(0.7, 0.15, 250);
console.log(okLch instanceof Color.OkLch); // true
console.log(lab instanceof Color.Lab); // trueAll channel inputs must be finite numbers. Alpha is clamped to 0–1 and hue is wrapped into 0–360 degrees. Other constructor channels deliberately retain finite extended values rather than being silently clipped, which allows out-of-gamut intermediate results.
Constructors and numeric factories retain negative chroma. Parsing lch() and oklch() strings clamps negative chroma to zero.
Use to(space) with any space() identifier from the table, or call a named conversion method. The xyz alias is accepted while parsing color(...), but conversion uses the canonical xyz-d65 identifier:
const color = Color.fromString('#663399');
const lab = color.to('lab');
const p3 = color.toDisplayP3();
const rgb = color.toRgb();The complete named conversion API is:
toA98Rgb()toDisplayP3()toDisplayP3Linear()toHex()toHsl()toHwb()toLab()toLch()toOkLab()toOkLch()toProPhotoRgb()toRec2020()toRgb()toSrgb()toSrgbLinear()toXyzD50()toXyzD65()
Alpha is preserved through conversions. Converting to the instance's existing space returns that same instance; other conversions return a new concrete instance.
When converting to HSL, colors whose computed HSL lightness is exactly 0% or 100% use zero hue and saturation. Extended colors at those boundaries therefore become black or white and cannot round-trip through HSL without losing their original channels.
Use foreground.composite(background) to composite a color over a background using source-over alpha compositing in sRGB. The result uses the foreground color's concrete class:
const foreground = Color.fromString('rgb(255 255 255 / 50%)');
const background = Color.fromString('#000');
const result = foreground.composite(background);Conversions do not automatically fit colors to the target gamut. Use fitGamut(target = 'srgb') when output must fit a bounded RGB gamut:
const vivid = Color.fromOkLch(0.72, 0.4, 30);
const displayable = vivid.fitGamut('srgb').toSrgb();Gamut fitting returns the original instance if it already fits the target gamut. Otherwise, it maps through OKLCH and uses a precision-bounded binary search to reduce chroma magnitude, preserving its sign, until the converted channels fall within the target range. Colors at or beyond the OKLCH lightness boundaries are fitted to black or white. Otherwise, it preserves OKLCH lightness. The result retains alpha and uses the source instance's color space. Supported targets are:
a98-rgbdisplay-p3display-p3-linearprophoto-rgbrec2020rgbsrgbsrgb-linear
Every concrete color supports these methods:
| Method | Result |
|---|---|
getAlpha() |
Alpha in the 0–1 range |
space() |
The current color-space identifier |
toObject() |
A new object containing the three channels and alpha |
composite(background) |
Source-over alpha composite calculated in sRGB |
luma() |
Relative sRGB luminance |
contrast(other) |
Ratio calculated from the relative luminance of two fully opaque colors |
label() |
The nearest CSS named color by Euclidean channel distance in the current color space |
contrast() requires both colors to be fully opaque. luma() ignores alpha. Extended out-of-range channels are not clamped before either calculation.
Channel-specific getters and copy methods depend on the concrete class:
| Class family | Getters | Copy methods |
|---|---|---|
| A98 RGB, Display P3, Display P3 Linear, Hex, ProPhoto RGB, Rec. 2020, RGB, sRGB, linear sRGB | getRed(), getGreen(), getBlue() |
withRed(), withGreen(), withBlue() |
| Lab and OKLab | getLightness(), getA(), getB() |
withLightness(), withA(), withB() |
| LCH and OKLCH | getLightness(), getChroma(), getHue() |
withLightness(), withChroma(), withHue() |
| HSL | getHue(), getSaturation(), getLightness() |
withHue(), withSaturation(), withLightness() |
| HWB | getHue(), getWhiteness(), getBlackness() |
withHue(), withWhiteness(), withBlackness() |
| XYZ D50 and XYZ D65 | getX(), getY(), getZ() |
withX(), withY(), withZ() |
All classes also provide withAlpha(alpha):
const original = Color.fromRgb(102, 51, 153);
const changed = original.withRed(120).withAlpha(0.5);
console.log(original.toString()); // rgb(102 51 153)
console.log(changed.toString()); // rgb(120 51 153 / 50%)toString(alpha = null, precision = 2, ...options) emits the native form listed in the spaces table. With alpha = null, alpha is included automatically only when it is below 1. Pass true to force it or false to omit it.
const color = Color.fromRgb(102.1234, 51, 153, 0.8);
color.toString(); // rgb(102.12 51 153 / 80%)
color.toString(false, 0); // rgb(102 51 153)
color.toHex().toString(); // #639cAdditional formatting methods and options are:
Rgb#getHex(alpha = false, shortenHex = true)returns hex digits without#. Hex output rounds and clamps RGB channels to 0–255 and encodes alpha as a rounded 0–255 byte when included.Rgb#toString(alpha = null, precision = 2, name = false)can prefer an exact CSS color name. Name matching requires the original RGB channels to be integers within 0–255.Hex#toString(alpha = null, precision = 2, shortenHex = true, name = false)can shorten hex and prefer a CSS color name matching the rounded, clamped RGB bytes. Itsprecisionargument is accepted for a consistent signature but is unused.toColorString(alpha = null, precision = 2)is the shared low-level serializer used by spaces whose native representation iscolor(...).
When name = true, a matching opaque keyword can be returned even with alpha = true. transparent is used only when alpha is included and equals zero. Passing alpha = false omits transparency even when names are preferred:
const transparentRed = Color.fromRgb(255, 0, 0, 0);
transparentRed.toString(null, 2, true); // transparent
transparentRed.toString(false, 2, true); // redFrost Color throws TypeError when:
- a color string is malformed or unsupported;
- a channel is not a finite number;
- contrast is requested with a translucent color;
- an unknown conversion space is requested;
- gamut fitting is requested for an unsupported space;
- the abstract
Colorclass is constructed directly.
npm test
npm run lint
npm run buildFrost Color is released under the MIT License.