npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/h

Contrast

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 contrast

Color 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 mapping

Color 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);