Skip to main content

OKLCH & Color Space Conversion API

REAL DATA

Convert any CSS color between sRGB, Display P3, Rec2020, CIE Lab/LCh, OKLab/OKLCH and XYZ, with per-gamut flags, Delta-E differences and CSS gamut mapping.

Authoritative reference data or standards computation

Base URL
/api/color-spaces
Capabilities
3 routes
Last updated
July 30, 2026
Data sources · 3

GET /api/color-spaces/convert

Parses a CSS , converts it through CIE XYZ with linear Bradford adaptation when the white points differ, and reports the result plus its gamut status in every bounded RGB space — never clamped.

Live requestRuns against the public API
No key required
GET/api/color-spaces/convert?color=%23ff5500&to=oklch

Request parameters

Any absolute CSS Color 4 <color>: #rgb/#rgba/#rrggbb/#rrggbbaa, one of the 148 named colors, rgb()/rgba() (legacy comma or modern space form), hsl()/hsla(), hwb(), lab(), lch(), oklab(), oklch(), or color() with a predefined space. Percentages and angle units (deg, grad, rad, turn) are accepted; hue angles are normalized to [0, 360). Encode '#' as %23 and spaces as '+' in a URL.

Destination color space id. Required on purpose: an unnamed destination is a request without a meaning. 'xyz' is accepted as CSS's alias for 'xyz-d65' and is canonicalized, with a warning.

Decimal places for the rounded numbers in the response. Rounded towards +∞ per CSS Color 4 §16, so a component of exactly -0.125 becomes -0.12 at precision=2. Gamut verdicts and the reported difference figures are always computed from the UNROUNDED result, so precision can never flip a verdict.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/color-spaces/convert?color=%23ff5500&to=oklch"
const res = await fetch("https://randomapi.dev/api/color-spaces/convert?color=%23ff5500&to=oklch");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/color-spaces/convert?color=%23ff5500&to=oklch").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/color-spaces/convert?color=%23ff5500&to=oklch"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 3
color string required

Any absolute CSS Color 4 <color>: #rgb/#rgba/#rrggbb/#rrggbbaa, one of the 148 named colors, rgb()/rgba() (legacy comma or modern space form), hsl()/hsla(), hwb(), lab(), lch(), oklab(), oklch(), or color() with a predefined space. Percentages and angle units (deg, grad, rad, turn) are accepted; hue angles are normalized to [0, 360). Encode '#' as %23 and spaces as '+' in a URL.

allowed: 1 – 128
example: color=#ff5500
to enum required

Destination color space id. Required on purpose: an unnamed destination is a request without a meaning. 'xyz' is accepted as CSS's alias for 'xyz-d65' and is canonicalized, with a warning.

allowed: srgb | srgb-linear | display-p3 | display-p3-linear | a98-rgb | prophoto-rgb | rec2020 | xyz | xyz-d50 | xyz-d65 | lab | lch | oklab | oklch | hsl | hwb
example: to=oklch
precision int

Decimal places for the rounded numbers in the response. Rounded towards +∞ per CSS Color 4 §16, so a component of exactly -0.125 becomes -0.12 at precision=2. Gamut verdicts and the reported difference figures are always computed from the UNROUNDED result, so precision can never flip a verdict.

default: 5
allowed: 0 – 12
example: precision=4
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=input,inputSpace
exclude list

Return all fields except these (comma-separated).

example: exclude=roundingDeltaEOK
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 12
input string (CSS)

The color you supplied, serialized in ITS OWN space in modern CSS Color 4 syntax after any parse-time clamping, with percentages resolved and hue angles normalized to [0, 360) — proof of what was actually parsed.

example: color(srgb 1 0.33333 0)

inputSpace string

Detected source space: srgb for hex, named colors and rgb(); otherwise hsl, hwb, lab, lch, oklab, oklch or the color() space. color(xyz …) reports the canonical xyz-d65.

example: srgb

space string

Destination space id, canonicalized — 'xyz' is reported as 'xyz-d65'.

example: oklch

whitePoint string

Reference white of the destination space: D50 for lab, lch, xyz-d50 and prophoto-rgb, D65 for everything else.

example: D65

chromaticAdaptation string nullable

The adaptation that ran, e.g. 'linear Bradford, D65 to D50'. Null when the source and destination share a white point, which is exactly when no adaptation is performed.

example: null

components object

Destination components rounded to `precision`, keyed per space: {r,g,b} for RGB spaces, {x,y,z} for XYZ, {l,a,b} for lab/oklab, {l,c,h} for lch/oklch, {h,s,l} for hsl, {h,w,b} for hwb. A component that became missing is null, never omitted.

example: {"l":0.67589,"c":0.21747,"h":38.80221}

css string (CSS)

The result in modern, lossless CSS syntax, with `none` for a missing component. RGB and XYZ spaces use the color() form, which — unlike the legacy comma form — can express an out-of-gamut value.

example: oklch(0.67589 0.21747 38.80221)

hex string (hex color) nullable

8-bit #rrggbb (or #rrggbbaa when alpha < 1) ONLY when the color lies inside the sRGB gamut; null otherwise. Never a clipped hex — use /api/color-spaces/gamut to map an out-of-gamut color in. Hex is 8-bit by definition, so it is encoded from the EXACT sRGB value rather than from the rounded `components`, and `precision` does not apply to it.

example: #ff5500

alpha float (0-1)

Alpha carried through unchanged — never composited, never affecting the conversion (CSS clamps it to 0–1 at parse time). Reported rounded to `precision`, while the `css` alpha suffix is present whenever the exact alpha is not 1, so at precision=0 a semi-transparent color reads '/ 1'.

example: 1

inGamut object

Booleans keyed srgb, display-p3, a98-rgb, prophoto-rgb and rec2020 — the five bounded RGB gamuts, tested on the unrounded result with a tolerance of 1e-7 per component. srgb-linear shares sRGB's gamut and display-p3-linear shares Display P3's; lab, lch, oklab, oklch and xyz-* are unbounded and so are absent.

example: {"srgb":true,"display-p3":true,"a98-rgb":true,"prophoto-rgb":true,"rec2020":true}

powerlessComponents string[]

Components that CSS Color 4 §4.4.1 makes powerless and therefore missing after a conversion — in practice only 'hue', when the chroma or saturation falls at or below the destination's epsilon. Empty for a normal color.

example: []

roundingDeltaEOK float (deltaEOK)

ΔEOK between the exact result and the result read back from the rounded `components` — exactly what `precision` cost you, rounded to 6 decimals. 0 at high precision.

example: 0

Documented examples

Build-generated requests and complete responses
6
Hex to OKLCH
GET /api/color-spaces/convert?color=%23ff5500&to=oklch
{
  "data": {
    "input": "color(srgb 1 0.33333 0)",
    "inputSpace": "srgb",
    "space": "oklch",
    "whitePoint": "D65",
    "chromaticAdaptation": null,
    "components": {
      "l": 0.67589,
      "c": 0.21747,
      "h": 38.80221
    },
    "css": "oklch(0.67589 0.21747 38.80221)",
    "hex": "#ff5500",
    "alpha": 1,
    "inGamut": {
      "srgb": true,
      "display-p3": true,
      "a98-rgb": true,
      "prophoto-rgb": true,
      "rec2020": true
    },
    "powerlessComponents": [],
    "roundingDeltaEOK": 0.000003
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/convert",
    "params": {
      "color": "#ff5500",
      "to": "oklch",
      "precision": 5
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
OKLCH back to sRGB hex
GET /api/color-spaces/convert?color=oklch(0.637+0.237+25.331)&to=srgb
{
  "data": {
    "input": "oklch(0.637 0.237 25.331)",
    "inputSpace": "oklch",
    "space": "srgb",
    "whitePoint": "D65",
    "chromaticAdaptation": null,
    "components": {
      "r": 0.98266,
      "g": 0.1718,
      "b": 0.21307
    },
    "css": "color(srgb 0.98266 0.1718 0.21307)",
    "hex": "#fb2c36",
    "alpha": 1,
    "inGamut": {
      "srgb": true,
      "display-p3": true,
      "a98-rgb": true,
      "prophoto-rgb": true,
      "rec2020": true
    },
    "powerlessComponents": [],
    "roundingDeltaEOK": 0.000001
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/convert",
    "params": {
      "color": "oklch(0.637 0.237 25.331)",
      "to": "srgb",
      "precision": 5
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
CIE Lab (D50) of a purple, showing the Bradford adaptation
GET /api/color-spaces/convert?color=%237654cd&to=lab&precision=4
{
  "data": {
    "input": "color(srgb 0.4627 0.3294 0.8039)",
    "inputSpace": "srgb",
    "space": "lab",
    "whitePoint": "D50",
    "chromaticAdaptation": "linear Bradford, D65 to D50",
    "components": {
      "l": 44.3577,
      "a": 36.0479,
      "b": -58.9859
    },
    "css": "lab(44.3577 36.0479 -58.9859)",
    "hex": "#7654cd",
    "alpha": 1,
    "inGamut": {
      "srgb": true,
      "display-p3": true,
      "a98-rgb": true,
      "prophoto-rgb": true,
      "rec2020": true
    },
    "powerlessComponents": [],
    "roundingDeltaEOK": 0
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/convert",
    "params": {
      "color": "#7654cd",
      "to": "lab",
      "precision": 4
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
An out-of-gamut OKLCH color, reported rather than clamped
GET /api/color-spaces/convert?color=oklch(0.7+0.3+30)&to=srgb
{
  "data": {
    "input": "oklch(0.7 0.3 30)",
    "inputSpace": "oklch",
    "space": "srgb",
    "whitePoint": "D65",
    "chromaticAdaptation": null,
    "components": {
      "r": 1.17254,
      "g": -0.13948,
      "b": -0.12834
    },
    "css": "color(srgb 1.17254 -0.13948 -0.12834)",
    "hex": null,
    "alpha": 1,
    "inGamut": {
      "srgb": false,
      "display-p3": false,
      "a98-rgb": false,
      "prophoto-rgb": true,
      "rec2020": true
    },
    "powerlessComponents": [],
    "roundingDeltaEOK": 0.000001
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/convert",
    "params": {
      "color": "oklch(0.7 0.3 30)",
      "to": "srgb",
      "precision": 5
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Rec2020 lime into Display P3 (negative components preserved)
GET /api/color-spaces/convert?color=color(rec2020+0.42053+0.97978+0.00579)&to=display-p3&precision=6
{
  "data": {
    "input": "color(rec2020 0.42053 0.97978 0.00579)",
    "inputSpace": "rec2020",
    "space": "display-p3",
    "whitePoint": "D65",
    "chromaticAdaptation": null,
    "components": {
      "r": -0.350289,
      "g": 1.007067,
      "b": -0.144209
    },
    "css": "color(display-p3 -0.350289 1.007067 -0.144209)",
    "hex": null,
    "alpha": 1,
    "inGamut": {
      "srgb": false,
      "display-p3": false,
      "a98-rgb": false,
      "prophoto-rgb": true,
      "rec2020": true
    },
    "powerlessComponents": [],
    "roundingDeltaEOK": 0
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/convert",
    "params": {
      "color": "color(rec2020 0.42053 0.97978 0.00579)",
      "to": "display-p3",
      "precision": 6
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
A named color to XYZ D65
GET /api/color-spaces/convert?color=rebeccapurple&to=xyz-d65&precision=6
{
  "data": {
    "input": "color(srgb 0.4 0.2 0.6)",
    "inputSpace": "srgb",
    "space": "xyz-d65",
    "whitePoint": "D65",
    "chromaticAdaptation": null,
    "components": {
      "x": 0.124123,
      "y": 0.074925,
      "z": 0.309303
    },
    "css": "color(xyz-d65 0.124123 0.074925 0.309303)",
    "hex": "#663399",
    "alpha": 1,
    "inGamut": {
      "srgb": true,
      "display-p3": true,
      "a98-rgb": true,
      "prophoto-rgb": true,
      "rec2020": true
    },
    "powerlessComponents": [],
    "roundingDeltaEOK": 0
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/convert",
    "params": {
      "color": "rebeccapurple",
      "to": "xyz-d65",
      "precision": 6
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/color-spaces/difference

Computes CIEDE2000, CIE76 or deltaEOK between two colors and reports the coordinate space, white point and just-noticeable-difference convention the number is expressed in.

Live requestRuns against the public API
No key required
GET/api/color-spaces/difference?from=%23d32f2f&to=%23dc2626

Request parameters

First color. Any absolute CSS Color 4 <color>: #rgb/#rgba/#rrggbb/#rrggbbaa, one of the 148 named colors, rgb()/rgba() (legacy comma or modern space form), hsl()/hsla(), hwb(), lab(), lch(), oklab(), oklch(), or color() with a predefined space. Percentages and angle units (deg, grad, rad, turn) are accepted; hue angles are normalized to [0, 360). Encode '#' as %23 and spaces as '+' in a URL. All three metrics are symmetric, so swapping 'from' and 'to' returns the same number.

Second color. NOTE: on this route 'to' is a COLOR, not a space id — pass a space id to /api/color-spaces/convert?to=… instead. Any absolute CSS Color 4 <color>: #rgb/#rgba/#rrggbb/#rrggbbaa, one of the 148 named colors, rgb()/rgba() (legacy comma or modern space form), hsl()/hsla(), hwb(), lab(), lch(), oklab(), oklch(), or color() with a predefined space. Percentages and angle units (deg, grad, rad, turn) are accepted; hue angles are normalized to [0, 360). Encode '#' as %23 and spaces as '+' in a URL.

Difference formula. ciede2000 = CIE 142:2001 with kL=kC=kH=1; cie76 = plain Euclidean distance in CIELAB (ΔE*ab); oklab = ΔEOK, the Euclidean distance in OKLab (CSS Color 4 §20.2).

White point for the CIELAB coordinates the Lab-based metrics use. Omitted ⇒ d50, matching CSS Color 4's lab(), and the resolved value is echoed in meta.params. Rejected with a 400 for method=oklab, which is defined only at D65.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/color-spaces/difference?from=%23d32f2f&to=%23dc2626"
const res = await fetch("https://randomapi.dev/api/color-spaces/difference?from=%23d32f2f&to=%23dc2626");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/color-spaces/difference?from=%23d32f2f&to=%23dc2626").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/color-spaces/difference?from=%23d32f2f&to=%23dc2626"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 4
from string required

First color. Any absolute CSS Color 4 <color>: #rgb/#rgba/#rrggbb/#rrggbbaa, one of the 148 named colors, rgb()/rgba() (legacy comma or modern space form), hsl()/hsla(), hwb(), lab(), lch(), oklab(), oklch(), or color() with a predefined space. Percentages and angle units (deg, grad, rad, turn) are accepted; hue angles are normalized to [0, 360). Encode '#' as %23 and spaces as '+' in a URL. All three metrics are symmetric, so swapping 'from' and 'to' returns the same number.

allowed: 1 – 128
example: from=#d32f2f
to string required

Second color. NOTE: on this route 'to' is a COLOR, not a space id — pass a space id to /api/color-spaces/convert?to=… instead. Any absolute CSS Color 4 <color>: #rgb/#rgba/#rrggbb/#rrggbbaa, one of the 148 named colors, rgb()/rgba() (legacy comma or modern space form), hsl()/hsla(), hwb(), lab(), lch(), oklab(), oklch(), or color() with a predefined space. Percentages and angle units (deg, grad, rad, turn) are accepted; hue angles are normalized to [0, 360). Encode '#' as %23 and spaces as '+' in a URL.

allowed: 1 – 128
example: to=#dc2626
method enum

Difference formula. ciede2000 = CIE 142:2001 with kL=kC=kH=1; cie76 = plain Euclidean distance in CIELAB (ΔE*ab); oklab = ΔEOK, the Euclidean distance in OKLab (CSS Color 4 §20.2).

default: ciede2000
allowed: cie76 | ciede2000 | oklab
example: method=ciede2000
whitePoint enum

White point for the CIELAB coordinates the Lab-based metrics use. Omitted ⇒ d50, matching CSS Color 4's lab(), and the resolved value is echoed in meta.params. Rejected with a 400 for method=oklab, which is defined only at D65.

allowed: d50 | d65
example: whitePoint=d50
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=from,to
exclude list

Return all fields except these (comma-separated).

example: exclude=aboveJnd
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 11
from string (CSS)

Canonical serialization of the first color in its own space, at 6 decimals.

example: color(srgb 0.827451 0.184314 0.184314)

to string (CSS)

Canonical serialization of the second color in its own space, at 6 decimals.

example: color(srgb 0.862745 0.14902 0.14902)

method string

Metric applied.

example: ciede2000

metric string

The formula's full name and citation.

example: CIEDE2000 (CIE 142:2001), kL = kC = kH = 1

coordinateSpace string

Space the distance was computed in: 'lab' or 'oklab'.

example: lab

whitePoint string

D50 or D65 — the white the coordinates are relative to. Always present, including for oklab, which is always D65.

example: D50

fromCoordinates object

The exact {l, a, b} triple the metric consumed, unrounded to full double precision, so the answer is auditable.

example: {"l":49.508243,"a":65.311663,"b":41.617664}

toCoordinates object

The same for the second color.

example: {"l":50.42375,"a":68.166768,"b":44.700709}

deltaE float

The difference on this metric's own scale, rounded to 6 decimals.

example: 1.602137

jnd float

The just-noticeable-difference CONVENTION for this scale: 2 for the Lab-scale metrics, 0.02 for ΔEOK (CSS Color 4 §14.2.1 states both). A convention, not a perceptual guarantee — echoed on every response.

example: 2

aboveJnd boolean

Whether deltaE >= jnd. A rule of thumb, not a claim about what any person sees.

example: false

Documented examples

Build-generated requests and complete responses
5
CIEDE2000 between two brand reds
GET /api/color-spaces/difference?from=%23d32f2f&to=%23dc2626
{
  "data": {
    "from": "color(srgb 0.827451 0.184314 0.184314)",
    "to": "color(srgb 0.862745 0.14902 0.14902)",
    "method": "ciede2000",
    "metric": "CIEDE2000 (CIE 142:2001), kL = kC = kH = 1",
    "coordinateSpace": "lab",
    "whitePoint": "D50",
    "fromCoordinates": {
      "l": 47.884220393405464,
      "a": 63.084588287458,
      "b": 42.35633959551773
    },
    "toCoordinates": {
      "l": 48.798178518646765,
      "a": 67.69992590825264,
      "b": 48.80376418419756
    },
    "deltaE": 2.327811,
    "jnd": 2,
    "aboveJnd": true
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/difference",
    "params": {
      "from": "#d32f2f",
      "to": "#dc2626",
      "method": "ciede2000",
      "whitePoint": "d50"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
The same pair measured with deltaEOK
GET /api/color-spaces/difference?from=%23d32f2f&to=%23dc2626&method=oklab
{
  "data": {
    "from": "color(srgb 0.827451 0.184314 0.184314)",
    "to": "color(srgb 0.862745 0.14902 0.14902)",
    "method": "oklab",
    "metric": "ΔEOK — Euclidean distance in OKLab (CSS Color 4 §20.2)",
    "coordinateSpace": "oklab",
    "whitePoint": "D65",
    "fromCoordinates": {
      "l": 0.5679892564800153,
      "a": 0.1792952269010927,
      "b": 0.08902497369738949
    },
    "toCoordinates": {
      "l": 0.5770988190297078,
      "a": 0.19114882743146472,
      "b": 0.09876509664242511
    },
    "deltaE": 0.017843,
    "jnd": 0.02,
    "aboveJnd": false
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/difference",
    "params": {
      "from": "#d32f2f",
      "to": "#dc2626",
      "method": "oklab",
      "whitePoint": "d65"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Plain CIE76 between sRGB blue and yellow
GET /api/color-spaces/difference?from=blue&to=yellow&method=cie76
{
  "data": {
    "from": "color(srgb 0 0 1)",
    "to": "color(srgb 1 1 0)",
    "method": "cie76",
    "metric": "CIE76 ΔE*ab — Euclidean distance in CIELAB (CIE 1976)",
    "coordinateSpace": "lab",
    "whitePoint": "D50",
    "fromCoordinates": {
      "l": 29.56830197909668,
      "a": 68.28736460254686,
      "b": -112.0297095251052
    },
    "toCoordinates": {
      "l": 97.60700786450093,
      "a": -15.749828397827736,
      "b": 93.39359070595872
    },
    "deltaE": 232.142731,
    "jnd": 2,
    "aboveJnd": true
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/difference",
    "params": {
      "from": "blue",
      "to": "yellow",
      "method": "cie76",
      "whitePoint": "d50"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Two OKLCH steps one JND apart
GET /api/color-spaces/difference?from=oklch(0.7+0.1+120)&to=oklch(0.72+0.1+120)&method=oklab
{
  "data": {
    "from": "oklch(0.7 0.1 120)",
    "to": "oklch(0.72 0.1 120)",
    "method": "oklab",
    "metric": "ΔEOK — Euclidean distance in OKLab (CSS Color 4 §20.2)",
    "coordinateSpace": "oklab",
    "whitePoint": "D65",
    "fromCoordinates": {
      "l": 0.7000000000000002,
      "a": -0.050000000000000266,
      "b": 0.08660254037844389
    },
    "toCoordinates": {
      "l": 0.7200000000000001,
      "a": -0.05000000000000043,
      "b": 0.08660254037844389
    },
    "deltaE": 0.02,
    "jnd": 0.02,
    "aboveJnd": true
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/difference",
    "params": {
      "from": "oklch(0.7 0.1 120)",
      "to": "oklch(0.72 0.1 120)",
      "method": "oklab",
      "whitePoint": "d65"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
CIEDE2000 under a D65 white instead of CSS's D50
GET /api/color-spaces/difference?from=%23d32f2f&to=%23dc2626&whitePoint=d65
{
  "data": {
    "from": "color(srgb 0.827451 0.184314 0.184314)",
    "to": "color(srgb 0.862745 0.14902 0.14902)",
    "method": "ciede2000",
    "metric": "CIEDE2000 (CIE 142:2001), kL = kC = kH = 1",
    "coordinateSpace": "lab",
    "whitePoint": "D65",
    "fromCoordinates": {
      "l": 47.091796437247396,
      "a": 62.29571477311907,
      "b": 40.67533861817281
    },
    "toCoordinates": {
      "l": 47.936513484128184,
      "a": 66.94135255477917,
      "b": 46.91049108099008
    },
    "deltaE": 2.272058,
    "jnd": 2,
    "aboveJnd": true
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/difference",
    "params": {
      "from": "#d32f2f",
      "to": "#dc2626",
      "method": "ciede2000",
      "whitePoint": "d65"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/color-spaces/gamut

Reports whether a color fits a bounded RGB gamut and, when it does not, maps it in with a named CSS Color 4 gamut-mapping algorithm — returning the chroma actually reached, the hue drift and the deltaEOK cost.

Live requestRuns against the public API
No key required
GET/api/color-spaces/gamut?color=oklch%280.7+0.3+30%29

Request parameters

Any absolute CSS Color 4 <color>: #rgb/#rgba/#rrggbb/#rrggbbaa, one of the 148 named colors, rgb()/rgba() (legacy comma or modern space form), hsl()/hsla(), hwb(), lab(), lch(), oklab(), oklch(), or color() with a predefined space. Percentages and angle units (deg, grad, rad, turn) are accepted; hue angles are normalized to [0, 360). Encode '#' as %23 and spaces as '+' in a URL.

Destination RGB gamut. Only bounded RGB spaces are accepted: lab, lch, oklab, oklch and xyz-* have no gamut limits, so 'mapping' into them is a no-op that would look like an answer.

Algorithm. binary-search-minde = CSS Color 4 §14.2.1/§14.2.2 (ΔEOK, JND 0.02, epsilon 1e-4). raytrace = §14.2.5/§14.2.6, with the 64-bit 1e-12 constants. clip = naive per-component clamping (§14.1.1), NOT a CSS gamut-mapping algorithm, offered so you can see what your own Math.min/max does.

Decimal places for the rounded numbers in the response. Rounded towards +∞ per CSS Color 4 §16, so a component of exactly -0.125 becomes -0.12 at precision=2. Gamut verdicts and the reported difference figures are always computed from the UNROUNDED result, so precision can never flip a verdict.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/color-spaces/gamut?color=oklch(0.7+0.3+30)"
const res = await fetch("https://randomapi.dev/api/color-spaces/gamut?color=oklch(0.7+0.3+30)");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/color-spaces/gamut?color=oklch(0.7+0.3+30)").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/color-spaces/gamut?color=oklch(0.7+0.3+30)"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 4
color string required

Any absolute CSS Color 4 <color>: #rgb/#rgba/#rrggbb/#rrggbbaa, one of the 148 named colors, rgb()/rgba() (legacy comma or modern space form), hsl()/hsla(), hwb(), lab(), lch(), oklab(), oklch(), or color() with a predefined space. Percentages and angle units (deg, grad, rad, turn) are accepted; hue angles are normalized to [0, 360). Encode '#' as %23 and spaces as '+' in a URL.

allowed: 1 – 128
example: color=oklch(0.7 0.3 30)
gamut enum

Destination RGB gamut. Only bounded RGB spaces are accepted: lab, lch, oklab, oklch and xyz-* have no gamut limits, so 'mapping' into them is a no-op that would look like an answer.

default: srgb
allowed: srgb | display-p3 | a98-rgb | prophoto-rgb | rec2020
example: gamut=srgb
method enum

Algorithm. binary-search-minde = CSS Color 4 §14.2.1/§14.2.2 (ΔEOK, JND 0.02, epsilon 1e-4). raytrace = §14.2.5/§14.2.6, with the 64-bit 1e-12 constants. clip = naive per-component clamping (§14.1.1), NOT a CSS gamut-mapping algorithm, offered so you can see what your own Math.min/max does.

default: binary-search-minde
allowed: binary-search-minde | raytrace | clip
example: method=raytrace
precision int

Decimal places for the rounded numbers in the response. Rounded towards +∞ per CSS Color 4 §16, so a component of exactly -0.125 becomes -0.12 at precision=2. Gamut verdicts and the reported difference figures are always computed from the UNROUNDED result, so precision can never flip a verdict.

default: 5
allowed: 0 – 12
example: precision=4
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=input,gamut
exclude list

Return all fields except these (comma-separated).

example: exclude=boundaryCase
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 15
input string (CSS)

Canonical serialization of the input in its own space.

example: oklch(0.7 0.3 30)

gamut string

Destination gamut id.

example: srgb

whitePoint string

Reference white of the destination gamut — D50 for prophoto-rgb, D65 otherwise.

example: D65

inGamut boolean

Whether the INPUT already fits, before any mapping, with a tolerance of 1e-7 per component. When true the color is returned unchanged (a relative colorimetric intent).

example: false

method string

Method applied.

example: binary-search-minde

algorithm string

The named algorithm and its spec section.

example: Binary Search Gamut Mapping with Local MINDE (CSS Color 4 §14.2.1)

mapped string (CSS)

The mapped color serialized in the destination gamut.

example: color(srgb 1 0.34514 0.26458)

mappedHex string (hex color) nullable

#rrggbb (or #rrggbbaa when alpha < 1) — only when gamut is srgb; null for every other gamut, because hex has no meaning outside sRGB.

example: #ff5843

components object

Mapped {r, g, b} in the destination gamut, rounded. A mapped color ends with the clip the algorithms mandate, so it is inside [0, 1]; an input that already fitted is passed through and can sit up to 1e-7 outside it.

example: {"r":1,"g":0.34514,"b":0.26458}

originalChroma float

OKLCh chroma of the input.

example: 0.3

mappedChroma float

OKLCh chroma actually reached — the number the caller came for.

example: 0.2062

chromaReduction float

originalChroma - mappedChroma. Positive for every ordinary mapping, but NOT guaranteed. The mandatory final local clip can nudge the chroma up near white: the largest increase on the wide sweep documented in the overview is +0.0050 (clip into display-p3) and +0.0026 (binary-search-minde), while raytrace never raised it in a physically realizable gamut. In prophoto-rgb — whose primaries are not physically realizable — the ray trace can land as much as 1.34 above where it started. Any increase above 0.02 raises a meta.warnings entry, so a diverged mapping cannot arrive without a signal.

example: 0.0938

deltaEOK float (deltaEOK)

ΔEOK between the input and the mapped result: the total perceptual cost.

example: 0.09532

hueShiftDegrees float

Absolute OKLCh hue drift, shortest way round (0–180). Every method's final local clip moves the hue: on the 287-colour sweep in the overview the CSS algorithms stay under 16° while clip reaches 69°, but at very low or very high lightness binary-search-minde rotates the hue about as far as clip does (wide-sweep maxima: 69.3° for binary-search-minde, 3.4° for raytrace, 94.2° for clip). 0 when either color is achromatic and therefore has no hue.

example: 0.1816

boundaryCase string nullable

The spec's lightness short-circuit when it fired: 'white (OKLCh L >= 1)' or 'black (OKLCh L <= 0)'; otherwise null.

example: null

Documented examples

Build-generated requests and complete responses
6
Map a vivid OKLCH color into sRGB
GET /api/color-spaces/gamut?color=oklch(0.7+0.3+30)
{
  "data": {
    "input": "oklch(0.7 0.3 30)",
    "gamut": "srgb",
    "whitePoint": "D65",
    "inGamut": false,
    "method": "binary-search-minde",
    "algorithm": "Binary Search Gamut Mapping with Local MINDE (CSS Color 4 §14.2.1)",
    "mapped": "color(srgb 1 0.34514 0.26458)",
    "mappedHex": "#ff5843",
    "components": {
      "r": 1,
      "g": 0.34514,
      "b": 0.26458
    },
    "originalChroma": 0.3,
    "mappedChroma": 0.2062,
    "chromaReduction": 0.0938,
    "deltaEOK": 0.09532,
    "hueShiftDegrees": 0.18163,
    "boundaryCase": null
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/gamut",
    "params": {
      "color": "oklch(0.7 0.3 30)",
      "gamut": "srgb",
      "method": "binary-search-minde",
      "precision": 5
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
The same color into Display P3
GET /api/color-spaces/gamut?color=oklch(0.7+0.3+30)&gamut=display-p3
{
  "data": {
    "input": "oklch(0.7 0.3 30)",
    "gamut": "display-p3",
    "whitePoint": "D65",
    "inGamut": false,
    "method": "binary-search-minde",
    "algorithm": "Binary Search Gamut Mapping with Local MINDE (CSS Color 4 §14.2.1)",
    "mapped": "color(display-p3 1 0.2851 0.19278)",
    "mappedHex": null,
    "components": {
      "r": 1,
      "g": 0.2851,
      "b": 0.19278
    },
    "originalChroma": 0.3,
    "mappedChroma": 0.26123,
    "chromaReduction": 0.03877,
    "deltaEOK": 0.04233,
    "hueShiftDegrees": 0.13546,
    "boundaryCase": null
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/gamut",
    "params": {
      "color": "oklch(0.7 0.3 30)",
      "gamut": "display-p3",
      "method": "binary-search-minde",
      "precision": 5
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
The same color with the ray-trace algorithm
GET /api/color-spaces/gamut?color=oklch(0.7+0.3+30)&method=raytrace
{
  "data": {
    "input": "oklch(0.7 0.3 30)",
    "gamut": "srgb",
    "whitePoint": "D65",
    "inGamut": false,
    "method": "raytrace",
    "algorithm": "Ray Trace Gamut Mapping (CSS Color 4 §14.2.5)",
    "mapped": "color(srgb 1 0.39624 0.31821)",
    "mappedHex": "#ff6551",
    "components": {
      "r": 1,
      "g": 0.39624,
      "b": 0.31821
    },
    "originalChroma": 0.3,
    "mappedChroma": 0.19151,
    "chromaReduction": 0.10849,
    "deltaEOK": 0.10849,
    "hueShiftDegrees": 0,
    "boundaryCase": null
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/gamut",
    "params": {
      "color": "oklch(0.7 0.3 30)",
      "gamut": "srgb",
      "method": "raytrace",
      "precision": 5
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
What naive clipping does to the same color
GET /api/color-spaces/gamut?color=oklch(0.7+0.3+30)&method=clip
{
  "data": {
    "input": "oklch(0.7 0.3 30)",
    "gamut": "srgb",
    "whitePoint": "D65",
    "inGamut": false,
    "method": "clip",
    "algorithm": "Naive per-component clipping (CSS Color 4 §14.1.1) — not a CSS gamut-mapping algorithm",
    "mapped": "color(srgb 1 0 0)",
    "mappedHex": "#ff0000",
    "components": {
      "r": 1,
      "g": 0,
      "b": 0
    },
    "originalChroma": 0.3,
    "mappedChroma": 0.25768,
    "chromaReduction": 0.04232,
    "deltaEOK": 0.08364,
    "hueShiftDegrees": 0.76612,
    "boundaryCase": null
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/gamut",
    "params": {
      "color": "oklch(0.7 0.3 30)",
      "gamut": "srgb",
      "method": "clip",
      "precision": 5
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
An in-gamut color is returned unchanged
GET /api/color-spaces/gamut?color=%232563eb
{
  "data": {
    "input": "color(srgb 0.1451 0.38824 0.92157)",
    "gamut": "srgb",
    "whitePoint": "D65",
    "inGamut": true,
    "method": "binary-search-minde",
    "algorithm": "Binary Search Gamut Mapping with Local MINDE (CSS Color 4 §14.2.1)",
    "mapped": "color(srgb 0.1451 0.38824 0.92157)",
    "mappedHex": "#2563eb",
    "components": {
      "r": 0.1451,
      "g": 0.38824,
      "b": 0.92157
    },
    "originalChroma": 0.21521,
    "mappedChroma": 0.21521,
    "chromaReduction": 0,
    "deltaEOK": 0,
    "hueShiftDegrees": 0,
    "boundaryCase": null
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/gamut",
    "params": {
      "color": "#2563eb",
      "gamut": "srgb",
      "method": "binary-search-minde",
      "precision": 5
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Rec2020 lime mapped into Display P3
GET /api/color-spaces/gamut?color=color(rec2020+0.42053+0.97978+0.00579)&gamut=display-p3
{
  "data": {
    "input": "color(rec2020 0.42053 0.97978 0.00579)",
    "gamut": "display-p3",
    "whitePoint": "D65",
    "inGamut": false,
    "method": "binary-search-minde",
    "algorithm": "Binary Search Gamut Mapping with Local MINDE (CSS Color 4 §14.2.1)",
    "mapped": "color(display-p3 0 0.99524 0.1171)",
    "mappedHex": null,
    "components": {
      "r": 0,
      "g": 0.99524,
      "b": 0.1171
    },
    "originalChroma": 0.40545,
    "mappedChroma": 0.36158,
    "chromaReduction": 0.04387,
    "deltaEOK": 0.04604,
    "hueShiftDegrees": 1.74345,
    "boundaryCase": null
  },
  "meta": {
    "endpoint": "color-spaces",
    "route": "/gamut",
    "params": {
      "color": "color(rec2020 0.42053 0.97978 0.00579)",
      "gamut": "display-p3",
      "method": "binary-search-minde",
      "precision": 5
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

About this API

Coverage & behavior

Converts a CSS <color> between every colour space CSS Color 4 defines, measures the difference between two colours, and maps an out-of-gamut colour into a bounded RGB gamut with the spec's named algorithms. Nothing is randomly generated and nothing is silently clipped.

Implemented against CSS Color Module Level 4, W3C Candidate Recommendation Draft, 28 July 2026 — a Candidate Recommendation Draft, not a Recommendation; it republished twice in July 2026 alone. The exact snapshot is pinned by dated URL and SHA-256 (94a6c45541721f38…), and a committed script re-derives the five RGB→XYZ matrices from the spec's own chromaticity tables and the two Bradford matrices from the published cone-response matrix — in exact rational arithmetic — checks every forward/inverse pair against the identity, checks that the OKLab matrices really are the recalculated ones (they must map the D65 white to oklab(1 0 0)), and fails on drift.

The pipeline, step by step (§11)

  1. Parse a CSS <color>, applying CSS's own parse-time clamping — the only clamping performed on an input, and always reported in meta.warnings.
  2. Linearize: undo the destination-independent transfer function, over the extended range so negative components reflect through the origin instead of being clamped.
  3. Linear RGB → CIE XYZ at the source space's white.
  4. Adapt with a linear Bradford transform only if the destination's white differs. chromaticAdaptation is non-null exactly when this step ran.
  5. XYZ → the destination's rectangular form, then apply the destination's transfer function.
  6. Convert to the polar form if the destination is cylindrical — at which point a powerless component may become missing.

White points, explicitly

D65: srgb, srgb-linear, display-p3, display-p3-linear, a98-rgb, rec2020, xyz-d65, hsl, hwb, oklab, oklch. D50: lab, lch, xyz-d50, prophoto-rgb.

prophoto-rgb is the only D50 RGB space, which inverts the usual rule: it needs no adaptation to reach lab() and it does need one to reach oklab() (§10.10/§10.11). Getting this backwards produces answers that look plausible and are wrong by about one Lab unit, so every response states its whitePoint and names the adaptation when one ran.

Nothing is silently clamped

/convert returns out-of-range components verbatim — color(rec2020 0.42053 0.97978 0.00579) in display-p3 really is color(display-p3 -0.350289 1.00707 -0.144209), negatives intact. hex is null outside the sRGB gamut rather than a clipped lie, inGamut reports all five bounded RGB gamuts, and every gamut verdict is computed on unrounded values with a 1e-7 per-component tolerance, so precision can never flip one. The only clamping anywhere in this endpoint is (a) CSS's own parse-time clamping of the input, always warned about, and (b) the final clip() step the CSS gamut-mapping algorithms themselves mandate on /gamut.

Powerless components become missing

A grey has no hue. #808080 reaches an OKLCh chroma of 3.3e-16 with atan2 returning a meaningless 180°, so — per §4.4.1 — the hue is reported as null in components, listed in powerlessComponents and serialized as none: oklch(0.59987 0 none). The thresholds are the spec's own tabulated ones, and this API follows the tabulated <=/>= forms (lch C ≤ 0.0015, oklch C ≤ 0.000004, hsl S ≤ 0.001, hwb W + B ≥ 99.999) rather than the normative prose's "less than", which differ only at a single measure-zero point. A hue you write yourself is never powerless — an identity conversion such as ?color=oklch(0.5 0 30)&to=oklch echoes the 30 back, exactly as §4.4.1 requires. That also means a css containing none cannot be fed straight back into this API: replace it with the explicit 0 that §4.4 says a missing component behaves as.

/gamut: what the CSS algorithms actually do

Both implemented CSS algorithms aim at constant-lightness, constant-hue chroma reduction in OKLCh — and both finish with a local clip, so the returned colour's OKLCh lightness and hue are not exactly the input's. Measured on this implementation over the 287 out-of-sRGB colours in a sweep of C = 0.3 × L 0.2→0.9 in 0.1 steps × hue 0→350 in 10° steps:

method max L drift max hue drift max ΔEOK from the input
binary-search-minde (§14.2.1) 0.018 15.7° 0.265
raytrace (§14.2.5) 0.00026 2.9° 0.266
clip (§14.1.1, not a CSS GMA) 0.186 69.0° 0.281

On that sweep the two CSS algorithms disagree by at most 0.073 ΔEOK measured as the gap between their ΔEOK-from-the-input, and by 0.076 ΔEOK measured between the two colours they return — two different quantities that both get called "disagreement", so both are quoted here. The spec lets a browser pick either algorithm, and they measurably differ.

That sweep is deliberately mild: it holds C at 0.3 and stops at L = 0.9. The extremes live at the two ends of the lightness range, so there is a second, wider sweep behind the numbers below — for each gamut, OKLCh L 0.01→0.995 in 0.005 steps × hue 0→357 in 3° steps × C 0.05→0.45 in 0.02 steps, keeping only the colours outside that gamut and discarding the lightness short-circuits: 316k–410k colours per gamut. Across its four physically realizable gamuts (srgb, display-p3, a98-rgb, rec2020) the worst cases are:

method max L drift max hue drift max chroma increase max ΔEOK from the input max disagreement with the other CSS algorithm
binary-search-minde 0.0196 69.3° +0.0026 0.448 0.193 in cost, 0.195 between the two results
raytrace 0.00085 3.4° never raised it once 0.448 (same pair)
clip 0.367 94.2° +0.0050 0.543

So the honest statement about hue is not "the CSS algorithms preserve it". raytrace really does stay within about 3°, but at very low or very high lightness binary-search-minde rotates the hue about as far as clip does. What the ray trace buys instead is proximity to the true gamut boundary: on an sRGB sweep of L 0.05→0.95 in 0.025 steps × hue 0→350 in 10° steps × C 0.1→0.4 in 0.05 steps (7,935 out-of-gamut colours), against the exact constant-lightness, constant-hue boundary chroma located by 80-step bisection, raytrace lands within 0.038 of it, binary-search-minde within 0.105 and clip within 0.293.

clip is not claimed to be worse. On the same 287 colours naive clipping was closer in ΔEOK than binary-search-minde on 192 of them (67%). What makes it unusable is the hue: up to 69° of rotation against 15.7°. Clipping optimises nothing in particular; that is why CSS does not use it. Both numbers are reported and no ranking beyond them is claimed.

prophoto-rgb is the one gamut to treat carefully. Its primaries are, in the spec's own words, hyper-saturated and not physically realizable, so a constant-lightness, constant-hue ray can leave and re-enter its cube — "the chroma at which the colour enters the gamut" is not even single-valued. On the wide sweep the two CSS algorithms differ by up to 1.35 ΔEOK there (1.41 between the two colours returned) against 0.19 in the four realizable gamuts, because at very low lightness the §14.2.5 ray trace stops converging altogether rather than merely landing further out. ?color=oklch(0.01 0.07 195)&gamut=prophoto-rgb&method=raytrace returns color(prophoto-rgb 0.026813 0.008508 1) — a fully saturated blue of OKLCh chroma 1.411 for an input chroma of 0.07, 1.357 ΔEOK (≈68 JND) away, which is not a nearby colour at all — where binary-search-minde returns a near-black 0.0053 away. Both answers are inside the gamut and method says which one you asked for; on top of that, any mapping that raises the OKLCh chroma by more than 0.02 says so in meta.warnings, so a diverged result cannot arrive without a signal.

For the same reason chromaReduction is not guaranteed positive, from two separate causes. The mandatory final local clip nudges it slightly negative near white: the largest increase on the wide sweep is +0.0050, by clip into display-p3 at oklch(0.995 0.23 99), and +0.0026 by binary-search-minde at oklch(0.97 0.23 105), while raytrace did not raise the chroma once in a realizable gamut. Separately, in prophoto-rgb the ray trace can land 1.34 above where it started — that is the divergence above, and the case the warning fires on.

Difference metrics

/difference offers ciede2000 (CIE 142:2001, kL = kC = kH = 1 — the same implementation /api/color-vision uses, validated against all 34 published Sharma, Wu & Dalal vectors), cie76 (plain Euclidean distance in CIELAB) and oklab (ΔEOK, §20.2). The Lab-based metrics default to CSS's D50 white and accept whitePoint=d65; ΔEOK is defined only at D65 and rejects the parameter. CIE94 is deliberately not offered: it is asymmetric in reference/sample and comes in two parameter sets (graphic arts kL=1, textiles kL=2), so offering it without forcing that choice invites a wrong answer — and CIEDE2000 supersedes it.

The other colour endpoints, and what this one is not

  • /api/colors generates colours and harmony palettes in hex/rgb/hsl. It converts nothing between spaces.
  • /api/color-contrast answers one accessibility question: the WCAG 2.2 ratio for two opaque sRGB hex colours.
  • /api/color-vision simulates colour-vision deficiency. Its CIEDE2000 figures are computed in a D65-relative CIELAB — the white sRGB's own primaries define — while CSS's lab(), and therefore this endpoint, is D50. The same colour has different Lab coordinates on the two pages; neither is wrong, and this endpoint states its white point on every response.
  • This endpoint is the measurement and conversion layer: no randomness, no accessibility verdict, no vision model.

Deliberately not implemented: CSS Color 5 (color-mix(), relative colour syntax, contrast-color(), light-dark(), device-cmyk()); none components on input — §4.4 makes a missing component behave as zero everywhere except when combining two colours, which this endpoint never does, so none on input could only ever mean 0 and is rejected in favour of writing that 0; currentcolor, transparent and system colours; calc() and every other math function; custom @color-profile spaces; the §14.2.3 EdgeSeeker gamut mapping, whose §14.2.4 pseudocode section is still an empty heading upstream; CSSOM legacy serialization (§16.2.2's rgb(255, 85, 0) form clamps to 0–255 and cannot express an out-of-gamut result, and §16.1.1's 8-bit alpha rule is a comma-form compatibility rule); ICC profiles, CMYK, display calibration, image processing and spectral data. Two documented quirks of the spec are also worth knowing: rec2020 here uses CSS's BT.1886 pure 2.4 power law, not the BT.2020 OETF with its 1.0993/0.0181 breakpoint, so an ITU-based library will disagree in the last decimals; and §10.7's inline ProPhoto linearization is misprinted (Math.pow(c, 1.8), which breaks negative components) — the §19 sample-code form is what runs here.

Use it for

  • Port a Tailwind v4 OKLCH palette back to hex for an email template with no oklch() support
  • Fail a CI check when a design token's OKLCH chroma leaves the sRGB gamut
  • Map a Display P3 brand color into sRGB with the CSS gamut-mapping algorithm instead of a naive clamp
  • Decide whether two candidate brand reds differ by more than one just-noticeable difference before merging a token change
  • Produce CIE Lab (D50) coordinates so a screen color can be compared against a spectrophotometer reading
  • Build an OKLCH lightness ramp and record each step's sRGB, Display P3 and Rec2020 gamut status

Frequently asked questions

How do I convert a hex color to OKLCH?

GET /api/color-spaces/convert?color=%23ff5500&to=oklch — encode the # as %23. At the default precision of 5 that returns oklch(0.67589 0.21747 38.80221), plus the color's gamut status in all five bounded RGB spaces.

Which white point do the Lab and XYZ results use?

lab, lch and xyz-d50 are D50, matching CSS Color 4's lab(); oklab, oklch, xyz-d65 and every RGB space except prophoto-rgb are D65. Every response states its whitePoint and names the linear Bradford adaptation in chromaticAdaptation when one was applied.

What happens to an OKLCH color that is outside the sRGB gamut?

Nothing is clipped. /convert returns the true out-of-range components, inGamut.srgb is false and hex is null rather than a clamped lie. Use /api/color-spaces/gamut to map it in.

Does the API implement the CSS gamut mapping algorithm?

Yes, both of the ones the spec gives pseudocode for: method=binary-search-minde is §14.2.1 (ΔEOK, JND 0.02) and method=raytrace is §14.2.5. A browser may pick either, and they measurably disagree — up to 0.073 ΔEOK on the 287-colour sRGB sweep documented in the overview, up to 0.19 on the wider sweep beside it, and up to 1.35 in prophoto-rgb, where the ray trace stops converging. The §14.2.3 EdgeSeeker algorithm is not implemented because its pseudocode section upstream is still empty.

Why is the hue null for grey colors?

CSS Color 4 §4.4.1 makes a hue powerless below each space's epsilon and requires conversion to turn it into a missing component. #808080 reaches an OKLCh chroma of 3.3e-16, so the hue comes back as null and serializes as none instead of a meaningless angle.

Which delta-E formulas does /difference support?

ciede2000 (CIE 142:2001), cie76 (Euclidean CIELAB) and oklab (ΔEOK, CSS Color 4 §20.2). CIE94 is deliberately excluded: it is asymmetric in reference/sample and has two parameter sets, so it cannot be offered without forcing a choice the caller did not make.

Standards & references