@newtonedev/colors
v1.1.4
Published
Color scale engine — produces accessible color scales from declarative parameters
Readme
newtone-colors
Pure TypeScript OKLCH color toolkit. Zero dependencies.
Produces accessible, perceptually uniform color scales from declarative parameters. Every output is guaranteed in-gamut — no fallbacks, no clamping after the fact.
Core concepts
All colors are in OKLCH — a perceptually uniform cylindrical space:
| Channel | Range | Meaning |
|---|---|---|
| L | 0–1 | Lightness (0 = black, 1 = white) |
| C | 0–~0.37 | Chroma (0 = achromatic) |
| h | 0–360° | Hue angle |
Scale generation
The primary output is generateScale — a discretized OKLCH curve from lightest to darkest, with chroma at each step proportional to the gamut boundary:
import { generateScale, oklchToHex } from "newtone-colors";
const scale = generateScale({ hue: 264, steps: 11 });
const hexValues = scale.map(oklchToHex);ScaleOptions
| Option | Type | Default | Description |
|---|---|---|---|
| contrast.light | number | 1 | How light the lightest step is (0–1). 0 = near-white, 1 = pure white. |
| contrast.dark | number | 1 | How dark the darkest step is (0–1). 0 = near-black, 1 = pure black. |
| hue | number | — | Hue angle in degrees (0–360) |
| chroma.amount | number | 0 | How much of the gamut to use (0–1). 0 = achromatic, 1 = maximum. |
| chroma.balance | number | 0.5 | How chroma is distributed along the scale (0 = toward lightest, 1 = toward darkest). |
| isP3 | boolean | false | Use Display P3 gamut instead of sRGB. |
| grading | Grading | — | Global hue grading shared across all palettes. |
| shift | Shift | — | Per-palette one-sided hue shift. Defaults to the dark end; set light: true for the light end. |
Key color workflow
Derive scale parameters from a known color, then locate it in the generated scale:
import { keyColor, generateScale, findNearest, oklchToHex } from "newtone-colors";
const key = keyColor("#3B82F6", { contrast: { light: 0, dark: 0 } });
const scale = generateScale({ ...key, steps: 11 });
const { index } = findNearest(key.resolved.oklch, scale);
// scale[index] is the step closest to the original color
console.log(oklchToHex(scale[index]));keyColor derives hue and chroma (amount + balance) from the color and returns them ready to spread into ScaleOptions. The chroma.balance is computed from where the color's lightness falls within the scale range, so the chroma distribution is centered around the key color's position.
The reverse direction — step to hex — is already covered by oklchToHex(scale[index]).
Hue grading
Shift hues at the light or dark end of the scale toward a target hue, using vector interpolation in OKLAB (no shortest-arc discontinuity at 180°):
import { generateScale } from "newtone-colors";
import type { Grading, Shift } from "newtone-colors";
// Global: affects all palettes equally
const grading: Grading = {
light: { hue: 145, amount: 0.15 }, // light end shifts toward 145°
dark: { hue: 25, amount: 0.15 }, // dark end shifts toward 25°
};
// Per-palette: one-sided, stronger. Omit `light` or set false for dark end.
const shift: Shift = {
hue: 200,
amount: 0.3,
// light: true ← uncomment to target the light end instead
};
const scale = generateScale({ hue: 264, grading, shift });Dynamic range
Map slider values (0–1) to lightness bounds. The defaults keep the scale away from blinding white and unusable black:
import { resolveLightest, resolveDarkest } from "newtone-colors";
const lightL = resolveLightest(0); // → 0.96 (MIN_LIGHTEST_L)
const darkL = resolveDarkest(0); // → 0.16 (MAX_DARKEST_L)
const scale = generateScale({ contrast: { light: 0, dark: 0 }, hue: 264, steps: 11 });See docs/constants.md for all hardcoded values and their rationale.
Gamut utilities
import { isInGamut, gamutMap, maxChroma, oklchToSrgb } from "newtone-colors";
const rgb = oklchToSrgb({ L: 0.7, C: 0.2, h: 264 });
isInGamut(rgb); // true/false
gamutMap({ L: 0.7, C: 0.3, h: 264 }, "srgb"); // → in-gamut Oklch
maxChroma(0.6, 264, "srgb"); // → max C at this L/hContrast
import { wcagContrast, apcaContrast, contrastTextHex } from "newtone-colors";
import type { Srgb } from "newtone-colors";
const bg: Srgb = { r: 0.1, g: 0.1, b: 0.5 };
const fg: Srgb = { r: 1, g: 1, b: 1 };
wcagContrast(bg, fg); // WCAG 2.x contrast ratio
apcaContrast(bg, fg); // APCA-W3 Lc value
contrastTextHex(bg); // "#000000" or "#ffffff", whichever has better contrastColor resolution
Resolve any hex or OKLCH input to scale-ready parameters, with automatic gamut mapping if needed:
import { resolveColor } from "newtone-colors";
const result = resolveColor("#E74C3C");
result.hue; // extracted hue
result.chromaRatio; // chroma as fraction of gamut boundary → ScaleOptions.chroma.amount
result.wasRemapped; // true if the color was outside the target gamut
result.original; // original OKLCH before mappingColor math
import { deltaEOK, mix } from "newtone-colors";
// Perceptual distance (Euclidean in OKLAB)
deltaEOK(colorA, colorB);
// Interpolate between two OKLCH colors (shortest-arc hue)
mix(colorA, colorB, 0.5);