Gamut Mapping & Out-of-Gamut Handling
Learn to detect, constrain, and fit colors using inGamut(), within(), fit(), and output fit options to safely handle colors that exceed display gamuts.
Colors defined in wide-gamut spaces (Display P3, Rec.2020, etc.) may not be displayable on standard sRGB screens. Saturon gives you full control over:
- Detection β
inGamut() - Correction into a target gamut β
within() - Pre-normalizing the color in its own colorspace β
fit() - Output safety β
fitoption into()/toString()/toArray()/toObject()
Why Gamut Mapping Matters
| Device | Gamut | Example |
|---|---|---|
| Standard monitor | sRGB | Most web content |
| High-end laptop | Display P3 | Apple Retina, modern phones |
| TV / Cinema | Rec.2020 | HDR content |
Example:
color(display-p3 1 0 0) is pure red in P3, but out of range in sRGB β clipping or dulling occurs on standard screens.
Detect Out-of-Gamut Colors β Color.prototype.inGamut()
Check whether a color is safely representable in a target gamut.
Syntax
inGamut(gamut: ColorSpace | string, epsilon = 1e-5): booleanExample
import { Color } from "saturon";
const p3Red = Color.from("color(display-p3 1 0 0)");
const sRGBRed = Color.from("rgb(255 0 0)");
console.log(p3Red.inGamut("srgb")); // false
console.log(sRGBRed.inGamut("srgb")); // true
console.log(p3Red.inGamut("display-p3")); // trueWhen to Use
- Feature fallback colors or polyfills
- Design system token validation
- Unit tests preventing unsafe output
Fit a Color Inside Its Own Colorspace β Color.prototype.fit()
fit() adjusts the current color in its own colorspace, ensuring that its raw component values respect that colorspace's valid component ranges or gamut boundaries. This is useful when:
- You want the canonical, gamut-respecting version of the color before converting it.
- The original color was created with component values outside the valid range (e.g., manually constructed, interpolated, or procedurally generated).
- You want to ensure a color is βrealβ in its native model instead of letting the target space hide the out-of-gamut issue.
Syntax
fit(options?: { fit?: FitMethod }): ColorBehavior
Fits the color in its current colorspace, using the chosen method. Unlike within(), it does not convert spaces β it only normalizes the current one.
Example
const oklab = new Color("oklab", [0.5, -0.5, 0.2]);
// Not recommended: fits after converting to P3
const notRecommended = oklab.to("display-p3", { fit: "clip" });
// Recommended: fit Oklab first
const recommended = oklab.fit({ fit: "clip" }).to("display-p3");
console.log(notRecommended); // color(display-p3 0 0.60774 0)
console.log(recommended); // color(display-p3 0 0.57416 0)Why? notRecommended converts Oklab β P3, but the Oklab source values (e.g., a = -0.5 < min -0.4) were invalid. P3 conversion silently hides that. fit() ensures the source colorspace is physically valid before converting anywhere.
Note
Oklab is theoretically unbounded, but Saturon defines limits for clip. Methods like "css-gamut-map" or
"chroma-reduction" ignore the limits when the colorspace has no target gamut.
Fit a Color Into a New Gamut β Color.prototype.within()
within() re-maps the color into a different colorspace's gamut, using a selected perceptual or non-perceptual fitting method.
Syntax
within(gamut: ColorSpace, method: FitMethod = config.defaults.fit): ColorFit Methods
| Method | Behavior |
|---|---|
"clip" | Clamp values to the nearest boundary (fast, may shift hue) |
"chroma-reduction" | Reduces chroma while keeping hue/lightness more stable |
"css-gamut-map" | Smooth, perceptual CSS Color 4 method |
Example
const wide = Color.from("color(display-p3 1 0.8 0)");
console.log(wide.within("srgb").to("rgb")); // β rgb(255 201 0)
console.log(wide.within("srgb", "css-gamut-map").to("rgb")); // β rgb(255 205 46)Safe Output with fit Option
The output-level fit option applies gamut mapping only to the target colorspace of the formatting operation.
Applies To
to(type, { fit })toString({ fit })toArray({ fit })toObject({ fit })
Example
const p3Color = Color.from("color(display-p3 1 0 0)");
// Unsafe
console.log(p3Color.to("rgb", { fit: "none" })); // β rgb(279 -58 -38)
// Safe
console.log(p3Color.to("rgb", { fit: "clip" })); // β rgb(255 0 0)
console.log(p3Color.to("rgb", { fit: "css-gamut-map" })); // β rgb(255 52 40)Important
fit on output only affects the target gamut. It cannot correct the color's source space. For source-space
correction, use color.fit() (preferred) or within() (when you know the target gamut).
Set Global Default
import { configure } from "saturon/utils";
configure({ defaults: { fit: "css-gamut-map" } });
console.log(p3Color.to("rgb")); // Auto-fittedPractical Workflows
1. Design Token Export
function exportToken(color: Color, format = "hex-color") {
color = color.fit(); // ensure source is valid
if (!color.inGamut("srgb")) {
color = color.within("srgb", "css-gamut-map");
}
return color.to(format);
}2. Theme Fallback
const base = Color.from("color(display-p3 0.2 0.8 1)").fit();
const prefersP3 = window.matchMedia("(color-gamut: p3)").matches;
const display = prefersP3 ? base : base.within("srgb", "css-gamut-map");
document.body.style.color = display.to("rgb");3. Unit Tests
test("brand colors must be sRGB-safe", () => {
const colors = ["#ff5733", "#33b5e5", "color(display-p3 1 0 0)"].map(Color.from).map((c) => c.fit());
colors.forEach((c) => {
expect(c.within("srgb").inGamut("srgb")).toBe(true);
});
});Summary
| Goal | Method |
|---|---|
| Validate source validity | color.fit() |
| Check if target gamut accepts it | color.inGamut("srgb") |
| Convert into a safe gamut | color.within("srgb", "css-gamut-map") |
| Safe formatting | color.to("rgb", { fit: "css-gamut-map" }) |