Skip to content

Library

color-map has zero runtime dependencies and "sideEffects": false, so a bundler keeps only what you import. Import everything from color-map:

import { parse, toCss, semanticPalette, toTailwindTheme } from 'color-map'

The root re-exports the four subpaths below. With a bundler, importing from the root costs the same bytes as importing from a subpath. Without a bundler, for example in Node or in a <script type="module">, the root loads every module: about 26 ms instead of 3 ms as an ES module. Code that runs that way can import the subpath that holds what it needs instead.

Entry point Holds
color-map Everything below. The original API is still here too, deprecated: import it from color-map/legacy.
color-map/color The color core: parse, convert, serialize, gamut mapping, mix, resolve, contrast.
color-map/scale createScale, createContrastScale, scaleFromBrand, tonalPalette, createGradient.
color-map/palette harmony, harmonize, inverse, darkVariant, semanticPalette, validate.
color-map/tokens The output formats: toDtcg, toCssVariables, toTailwindTheme.
color-map/legacy The original API: createColors, createColorsFromMap, rgbHex, rgbaString. Unchanged.

A color is a plain object, { colorSpace, components, alpha? }, with CSS names for the spaces and null for none. It is JSON, so it survives JSON.stringify.

import { parse, toCss, convert, mix, contrast, onColor } from 'color-map'
const blue = parse('#3b82f6')
toCss(convert(blue, 'oklch')) // oklch(0.62308 0.18801 259.81453)
toCss(mix(blue, 'white', 0.2, { in: 'oklab' }))
contrast('#3b82f6', 'white') // 3.68, the WCAG 2 ratio
onColor(blue) // a text color that reaches 4.5 on it

From one brand color to a palette and a token file:

import { semanticPalette, validate, toTailwindTheme } from 'color-map'
const palette = semanticPalette({ brand: '#3b82f6', mode: 'light' })
validate(palette).ok // true: every pair meets its target
const css = toTailwindTheme(palette) // an @theme { ... } block
  • Spaces: srgb, srgbLinear, hsl, hwb, lab, lch, oklab, oklch, displayP3, displayP3Linear, a98Rgb, prophotoRgb, rec2020, xyzD50, xyzD65.
  • Read and write: parse, parseExpr, toCss, toHex, fromArgb, toArgb.
  • Convert: convert, convertComponents, createConverter, spaces.
  • Gamut: inGamut, toGamut (the CSS Color 4 algorithm, or clip).
  • Compare: equals, deltaEOk.
  • Color 5: mix follows CSS Color 5 §13 and resolve evaluates color-mix(), relative colors with a calc() subset, light-dark() and contrast-color().
  • Contrast: contrast, meets, onColor, findContrast, and the ContrastFn type for supplying another measure. The default is WCAG 2.
  • Narrow forms: createToHex, createInGamut, createToGamut, createDeltaEOk, createEquals, createMix, createContrast, createMeets, createOnColor, createFindContrast, each over only the spaces you give it. See below.

convert takes a space by name, so it bundles all 15 spaces. To parse and convert with only the spaces you use, pass them to createConverter. The converter throws for a space it was not given. See Size budgets for the difference; for example toHex costs about 3 kB, and createToHex(oklch) about 1.3 kB.

import { parse, toCss, createConverter, srgb, oklch } from 'color-map/color'
const convert = createConverter(srgb, oklch)
toCss(convert(parse('#3b82f6'), 'oklch')) // oklch(0.62308 0.18801 259.81453)

The helpers that convert inside also reach all 15 spaces: toHex, inGamut, toGamut, deltaEOk, equals, mix, contrast, meets, onColor and findContrast. Each has a narrow form, named create plus the helper’s name, that takes the spaces your colors are in and returns the helper over just those. The narrow helper takes the same arguments, except that it takes color objects, not CSS strings, so it does not carry parse. Run strings through parse first.

import { parse, createToGamut, createToHex, oklch } from 'color-map/color'
const toGamut = createToGamut() // srgb, oklch and oklab come with it
const toHex = createToHex(oklch) // srgb comes with it
toHex(toGamut(parse('oklch(0.7 0.3 150)'))) // #00c248

Each factory adds the spaces the helper works in, so you list only the spaces of your colors, and of the gamut or the in space when it is not the default:

Factory Adds
createToHex srgb
createInGamut srgb
createToGamut srgb, oklch, oklab
createDeltaEOk oklab
createEquals nothing
createMix oklab
createContrast, createMeets srgb, xyzD65
createOnColor, createFindContrast srgb, xyzD65, oklch, oklab

A narrow helper throws for a space it was not given, and the error names that space. resolve has no narrow form: it reads CSS, which can name any space.

  • createScale({ seed, steps }) with the presets tailwind-11, radix-12, tone-13 and even, or a number of steps.
  • createContrastScale({ seed, background, targets }) returns one step per WCAG 2 target, each with its measured ratio and whether it was met.
  • scaleFromBrand(brand, { mode }) puts the brand color exactly at step 9, with a Radix-like shape.
  • tonalPalette(hue, chroma, tones) holds an OKLCH hue and chroma across CIE L* tones, with Material’s 13 tones by default.
  • createGradient(stops, count, { in, hue }) returns count colors along a gradient through color stops. See Gradients.
  • harmony(color, scheme) for complementary, split, analogous, triadic and tetradic, in oklch, lch, hsl or hwb; harmonize nudges a color’s hue toward another; inverse flips lightness (or RGB).
  • darkVariant(scale, strategy) with tone-swap, mirror or lightness-invert.
  • semanticPalette({ brand, mode }) returns scales and role pairs (primary, primaryContainer, success, warning, danger, info, surface, surfaceVariant, outline), each with its background, text color, measured ratio and target.
  • validate(palette) returns { ok, issues }: pairs below target, non-monotonic scales, colors outside sRGB.

Each exporter takes a token tree: a color, a CSS color string, an array (a scale), an object of these nested to any depth, or a SemanticPalette as is. An array of 11 colors gets the keys 50 to 950; any other length gets 1 to N.

  • toDtcg(tree) returns a W3C design-token (2025.10) group; write it with JSON.stringify.
  • toCssVariables(tree, { selector, prefix, space }) returns one rule of custom properties.
  • toTailwindTheme(tree, { namespace, inline, space }) returns a Tailwind v4 @theme block. The default space is oklch. The export panel shows each output, and this site’s build compiles the Tailwind output with Tailwind v4.

The original API works only in sRGB, interpolates in gamma-encoded sRGB, rounds to integers and takes alpha as a separate argument. It stays in color-map/legacy for all of 3.x. The root color-map still exports it in 2.x, marked deprecated; 3.0 removes it from the root, so change from 'color-map' to from 'color-map/legacy' for these names.

Legacy Replacement
rgbHex(rgb) toHex(color)
rgbaString(rgba) toCss(color)
createColors(from, to, shades, alpha) createGradient([from, to], shades, { in: 'srgb' }). The results differ: createGradient does not round.
createColorsFromMap(colormap, shades, alpha) createGradient(stops, shades, { in: 'srgb' }). See From createColorsFromMap.

createGradient samples a gradient through color stops, the way a CSS gradient blends them. Use it for data-visualization color maps, such as blue to lime to yellow for a heatmap:

import { createGradient } from 'color-map'
const heatmap = createGradient(
[{ color: 'blue', at: 0 }, { color: 'lime', at: 0.6 }, { color: 'yellow', at: 1 }],
72,
{ in: 'oklab' }
)
  • A stop is { color, at? }, or a bare color. The color is a CSS color string or a color object.
  • at runs from 0 at the start to 1 at the end. Stops without at spread evenly between their neighbors, as in CSS. A position before an earlier stop’s moves up to it, and two stops at one position switch color there.
  • The count colors are evenly spaced from 0 to 1, so the first and last fall on the ends. Before the first stop and after the last, the color holds.
  • in and hue work as they do in mix(). The default space is oklab, and the colors come back in that space.
  • Alpha comes from each stop’s color, and is 1 when the color has none. Alpha changes linearly between stops, and the other channels are premultiplied by it, so a transparent stop does not tint its neighbor.

createColorsFromMap is deprecated. Turn its color map into stops and interpolate in srgb:

import { createGradient, toCss } from 'color-map'
const summer = [{ index: 0, rgb: [0, 128, 102] }, { index: 1, rgb: [255, 255, 102] }]
const stops = summer.map(({ index, rgb: [r, g, b, a = 1] }) => ({ color: `rgb(${r} ${g} ${b} / ${a})`, at: index }))
const colors = createGradient(stops, 60, { in: 'srgb' }).map((c) => toCss(c))

The results come close to the old ones, but do not match them:

  • Channels run from 0 to 1 and are not rounded to whole numbers. With two stops, each channel is within 0.5 of the old 0 to 255 value.
  • The old function started a new run at each inner stop, so it repeated that stop’s color. createGradient places every color on one even spacing.
  • The old alpha argument set alpha for the whole map. Here each stop carries its own alpha in its color.

Contrast follows luminance, and a tone is CIE L*. The tone-based scales (tone-13, tonalPalette) therefore let you pick a text pair by tone alone:

Tone gap WCAG 2 contrast
40 or more at least 3
50 or more at least 4.48, not exactly 4.5 (4.48 at the worst pair, tones 100 and 50)
100 holds 4.5

Do not rely on a gap of 50 for the 4.5 threshold. Use a larger gap, or check the pair with contrast or meets. semanticPalette measures every pair and validate reports any that fall short.

color() accepts display-p3-linear, which is in CSS Color 4. Browser support for it is unverified, so test it in the browsers you target before shipping it in a stylesheet. The library parses, converts and serializes it either way.

Each budget is a gzip limit enforced by size-limit in CI. The measured size is smaller. The table is read from size-limit in packages/color-map/package.json, so it matches what CI enforces. For how these sizes compare with other libraries, see Compare.

ImportWhat is measuredBudget (gzip)
existing API{ createColors, createColorsFromMap, rgbHex, rgbaString }0.45 kB
existing API from the root{ createColors, createColorsFromMap, rgbHex, rgbaString }0.45 kB
convert between two spaces{ createConverter, srgb, oklch }1.22 kB
parse and convert between two spaces{ parse, createConverter, srgb, oklch }3.95 kB
toHex over every space{ toHex }3.01 kB
toHex from oklch, narrow{ createToHex, oklch }1.31 kB
toGamut over every space{ toGamut }3.27 kB
toGamut, narrow{ createToGamut }1.61 kB
mix over every space{ mix }6.26 kB
mix, narrow{ createMix }1.52 kB
contrast helpers over every space{ contrast, meets, onColor, findContrast }6.77 kB
contrast helpers, narrow{ createContrast, createMeets, createOnColor, createFindContrast }2.39 kB
every converting helper over every space{ toHex, toGamut, inGamut, deltaEOk, equals, mix, contrast, meets, onColor, findContrast }7.63 kB
every converting helper, narrow{ createToHex, createToGamut, createInGamut, createDeltaEOk, createEquals, createMix, createContrast, createMeets, createOnColor, createFindContrast }3.29 kB
full color core{ parse, toCss, toHex, toGamut, inGamut, deltaEOk, equals, fromArgb, toArgb, convert, spaces }6.76 kB
core with mix{ parse, toCss, toGamut, mix }7.04 kB
core with mix and contrast{ parse, toCss, toGamut, mix, contrast, meets, onColor, findContrast }7.77 kB
scales*9.1 kB
palette*8.46 kB
tokens*7.59 kB
everything*10.71 kB