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

@panmdaa/colors

v0.4.2

Published

HCT color space, Material Design 3 dynamic themes, WCAG contrast utilities — zero dependencies

Readme

@panmdaa/colors

Generate perceptually balanced design system themes from a single seed color — zero dependencies.

@panmdaa/colors is a TypeScript library for color science and accessible design system generation. Built on the HCT (Hue-Chroma-Tone) color space, it creates complete light and dark palettes with guaranteed WCAG-compliant contrast from any seed color.

npm install @panmdaa/colors

Quick look

import { palette, contrastChecker, simulateCVD } from "@panmdaa/colors";

// Generate a full design system theme from a seed color
const theme = palette("#744c9d", { variant: "expressive" });
theme.light.primary;    // "#b091ce"
theme.dark.primary;     // "#dcb8ff"
theme.light.background; // "#fcfcff"

// Score any color pair 0–10 with WCAG thresholds + CVD simulation
contrastChecker("#ffffff", "#ff0000").score;       // 4.6
contrastChecker("#ffffff", "#ff0000").simulations.deuteranopia.score; // 3.8

// Simulate color blindness (Machado 2009 — same as Chrome DevTools)
simulateCVD("#ff0000", "protanopia");    // "#665900"
simulateCVD("#ff0000", "deuteranopia");  // "#998700"

// Check every on-* pair in a palette with a single call
paletteChecker(theme).summary; // { total: 30, passingAA: 30, ... }

Theme generation

15 theme variants for both light and dark:

import { palette } from "@panmdaa/colors";

// All variants
palette("#744c9d", { variant: "monochrome" });     // Grayscale
palette("#744c9d", { variant: "neutral" });        // Muted, neutral
palette("#744c9d", { variant: "tonal-spot" });     // Default — subtle, balanced tint
palette("#744c9d", { variant: "vibrant" });        // High chroma
palette("#744c9d", { variant: "expressive" });     // Rotated hues
palette("#744c9d", { variant: "fidelity" });       // Source color faithful
palette("#744c9d", { variant: "content" });        // Content-based
palette("#744c9d", { variant: "rainbow" });        // Rainbow spectrum
palette("#744c9d", { variant: "fruit-salad" });    // Colorful, playful
palette("#744c9d", { variant: "cmf" });            // Custom configurable variant
palette("#744c9d", { variant: "soft" });            // Softer, lower-chroma
palette("#744c9d", { variant: "muted" });           // Quiet, low-chroma
palette("#744c9d", { variant: "warm" });            // Warm-tinted hues
palette("#744c9d", { variant: "cool" });            // Cool-tinted hues
palette("#744c9d", { variant: "high-contrast" });   // Stronger chroma separation

You can also create your own variant by extending VariantScheme directly and overriding only the palette methods you need:

import { Hct, VariantScheme, SchemeVariant, palette } from "@panmdaa/colors";

class BrandSoft extends VariantScheme {
  constructor(sourceColorOrList: Hct | Hct[], isDark: boolean, contrastLevel: number) {
    super(sourceColorOrList, isDark, contrastLevel, SchemeVariant.TONAL_SPOT);
  }

  static override primaryPalette(sourceColorOrList: Hct | Hct[], isDark: boolean, _contrastLevel: number) {
    const source = Array.isArray(sourceColorOrList)
      ? sourceColorOrList[0] ?? Hct.fromInt(0)
      : sourceColorOrList;
    return this.tonalPalette(source.hue, isDark ? 16 : 20);
  }

  static override secondaryPalette(sourceColorOrList: Hct | Hct[], _isDark: boolean, _contrastLevel: number) {
    const source = Array.isArray(sourceColorOrList)
      ? sourceColorOrList[0] ?? Hct.fromInt(0)
      : sourceColorOrList;
    return this.tonalPalette(source.hue, 12);
  }

  static override tertiaryPalette(sourceColorOrList: Hct | Hct[], _isDark: boolean, _contrastLevel: number) {
    const source = Array.isArray(sourceColorOrList)
      ? sourceColorOrList[0] ?? Hct.fromInt(0)
      : sourceColorOrList;
    return this.tonalPalette((source.hue + 40) % 360, 24);
  }
}

const theme = palette("#744c9d", { variant: BrandSoft });

variant can be either one of the built-in string variants or a custom class that extends VariantScheme. The custom class receives the source HCT color, isDark, and contrastLevel in its constructor, and can override only the palette methods it needs.

Each returns a Theme with light and dark palettes of 53 color roles:

const theme = palette("#744c9d", { variant: "tonal-spot" });

theme.light.primary;                  // Primary brand color
theme.light["primary-dim"];           // Dimmed variant
theme.light["on-primary"];            // Text/icon on primary
theme.light["primary-container"];     // Primary container surface
theme.light["on-primary-container"];
theme.light["primary-fixed"];
theme.light["primary-fixed-dim"];
theme.light["on-primary-fixed"];
theme.light["on-primary-fixed-variant"];
theme.light.background;
theme.light.surface;
theme.light["surface-dim"];
theme.light["surface-bright"];
theme.light["surface-container-lowest"];
theme.light["surface-container-low"];
theme.light["surface-container"];
theme.light["surface-container-high"];
theme.light["surface-container-highest"];
theme.light["surface-variant"];
theme.light["on-surface"];
theme.light["on-surface-variant"];
theme.light.outline;
theme.light["outline-variant"];
theme.light.error;
theme.light["error-dim"];
theme.light["on-error"];
theme.light.shadow;
theme.light.scrim;
theme.light["surface-tint"];
theme.light["inverse-surface"];
theme.light["inverse-on-surface"];
theme.light["inverse-primary"];
// + secondary, tertiary with all their dim/container/fixed variants

Custom tokens

Extend the palette with your own tokens. Perfect for brand colors, accents, and design tokens:

const theme = palette("#6750a4", {
  variant: "tonal-spot",
  extraColors: {
    brand: "#ff6600",                       // direct color
    muted: { from: "primary" },              // copy from palette
    mutedBold: { from: "primary", adjust: { tone: -10 } },  // adjusted
    accent: { harmonize: "#ff0000" },        // harmonized with source
    random: { random: true },                // randomized near source
    duo: { mix: ["primary", "secondary"] },  // blend of palette keys
    sunset: { mix: ["#ff0000", "#ff8800"] }, // blend of colors
  },
});

theme.light.brand;            // "#ff6600"
theme.light["on-brand"];      // "#ffffff" — auto-generated foreground
theme.light.muted;            // matches primary
theme.light.mutedBold;        // primary, tone-10
theme.light.duo;              // HCT midpoint of primary + secondary
theme.light.sunset;           // HCT midpoint of red + orange

Key behaviour:

  • Names normalize to kebab-case: theme.light["my-color"]
  • Every non-on-* token gets an auto-generated on-{name} with ≥4.5:1 contrast
  • from-based tokens (including mix with palette keys) resolve per-mode (different in light/dark)
  • harmonize, random, and direct hex values are shared across modes

Gradients

Standalone HCT interpolation or inline in your palette:

import { Color } from "@panmdaa/colors";

// 5-step gradient from red to blue
const steps = Color.gradient("#ff0000", "#0000ff", 5);
steps[0]; // "#ff0000"
steps[2]; // midpoint (interpolated in HCT space)
steps[4]; // "#0000ff"

// Inline in extraColors — expands to {name}-N tokens
const theme = palette("#6750a4", {
  extraColors: {
    sunset: { gradient: { from: "#ff0000", to: "#0000ff", count: 5 } },
    ramp: { gradient: { from: "primary", to: "secondary", count: 3 } },
  },
});

theme.light["sunset-0"];     // first step of the gradient
theme.light["sunset-4"];     // last step
theme.light["on-sunset-0"];  // auto-generated foreground
theme.light["ramp-0"];       // gradient between light primary → secondary

from and to accept hex colors or palette key references ("primary", "secondary", etc.).

CSS generation

Turn any theme into CSS custom properties:

import { palette, generateCSS, generateCSSSheet } from "@panmdaa/colors";

const theme = palette("#6750a4", {
  extraColors: { brand: "#ff6600" },
});

// Quick CSS string
const css = generateCSS(theme);
// :root { --color-primary: #b091ce; --color-on-primary: ... }

// Full stylesheet with light/dark blocks
const sheet = generateCSSSheet(theme);
// :root { --color-primary: ... }
// @media (prefers-color-scheme: dark) { :root { ... } }

// Custom prefix and dark selector
generateCSSSheet(theme, {
  prefix: "--md-sys-",
  darkSelector: '[data-theme="dark"]',
});
// :root { --md-sys-primary: ... }
// [data-theme="dark"] { ... }

Accessibility

Contrast reports

Audit all foreground/background pairs in your theme:

import { palette, report } from "@panmdaa/colors";

const theme = palette("#6750a4");
const { pairs, summary } = report(theme);

summary;
// { total: 14, passingAA: 14, passingAALarge: 14, passingAAA: 12 }

// Works for dark mode too
report(theme, "dark");

// Each pair includes detailed info
pairs[0];
// { role: "primary", onRole: "on-primary", fg: "#b091ce", bg: "#1e192b", ratio: 11.2, AA: true, AALarge: true, AAA: true }

// Automatically includes extraColors tokens
const theme2 = palette("#6750a4", {
  extraColors: { brand: { from: "primary" } },
});
report(theme2).pairs.some(p => p.role === "brand"); // true

Contrast checker

Score any color pair from 0–10 with WCAG thresholds and optional color vision deficiency simulation:

import { contrastChecker } from "@panmdaa/colors";

// Single pair — includes CVD simulations by default
const score = contrastChecker("#ffffff", "#ff0000");
// { ratio: 4.0, score: 4.6, smallText: 4.3, largeText: 6.3 }
// score.simulations.protanopia   → { ratio: 6.4, score: 6.1, ... }
// score.simulations.deuteranopia → { ratio: 3.2, score: 3.8, ... }
// score.simulations.tritanopia   → { ratio: 4.0, score: 4.5, ... }

// Without CVD — returns plain ContrastScore
contrastChecker("#ffffff", "#ff0000", false);
// { ratio: 4.0, score: 4.6, smallText: 4.3, largeText: 6.3 }

Three scores per result:

| Field | Range | Thresholds | |-------|-------|------------| | score | 0–10 | Logarithmic scale: log2(ratio) / log2(21) × 10 | | smallText | 0–10 | AA (4.5:1) = 5, AAA (7:1) = 7 | | largeText | 0–10 | AA (3:1) = 5, AAA (4.5:1) = 7 |

Under the hood, CVD simulations use the Machado (2009) physiologically-based model — the same algorithm Chrome DevTools uses for "Emulate vision deficiencies". Both base and on colors are simulated before computing the contrast ratio.

Palette checker

Check every on-* pair in a generated palette with a single call — works with the full theme or a single mode:

import { palette, paletteChecker } from "@panmdaa/colors";

const pal = palette("#6750a4", {
  variant: "tonal-spot",
  extraColors: { warning: "#ff8800" },
});

// Full theme — light + dark + global summary
const check = paletteChecker(pal);
check.summary;
// { total: 30, passingAA: 30, passingAALarge: 30, passingAAA: 8 }

check.light.summary;
// { total: 15, passingAA: 15, passingAALarge: 15, passingAAA: 4 }

// Each pair has WCAG booleans and CVD simulations (by default)
check.light.pairs[0];
// { role: "primary", onRole: "on-primary", base: "#b091ce", on: "#1e192b",
//   score: { ratio: 6.04, ... }, AA: true, AALarge: true, AAA: false,
//   simulations: { protanopia: { base: "...", on: "...", score: {...}, AA: true, ... }, ... } }

// Single mode — pass light or dark directly
const lightCheck = paletteChecker(pal.light);
lightCheck.summary; // { total: 15, ... }

// Disable CVD simulations
const plain = paletteChecker(pal, false);

Each CVD simulation includes the simulated colors so you can see exactly what shifts:

const deuteranopia = check.light.pairs[0].simulations.deuteranopia;
deuteranopia.base;  // "#3f527b" — simulated background
deuteranopia.on;    // "#dae3fe" — simulated foreground
deuteranopia.AA;    // true — still passes

Color blindness simulation

Simulate how any color appears under the most common color vision deficiencies:

import { simulateCVD, simulateAllCVD } from "@panmdaa/colors";

// Single simulation
simulateCVD("#ff0000", "protanopia");    // "#665900" (red → brown)
simulateCVD("#ff0000", "deuteranopia");  // "#998700" (red → olive)
simulateCVD("#ff0000", "tritanopia");    // "#ff000e" (nearly unchanged)

// All three at once
simulateAllCVD("#ff0000");
// { protanopia: "#665900", deuteranopia: "#998700", tritanopia: "#ff000e" }

// Partial deficiency (anomalous trichromacy)
simulateCVD("#ff0000", "protanopia", "mild"); // "#a95000"

// White is unaffected (no chroma to lose)
simulateCVD("#ffffff", "protanopia"); // "#ffffff"

The simulation uses the Machado (2009) physiologically-based model with matrices from Machado (2010) — the same algorithm Chrome DevTools uses internally. The pipeline is sRGB → linear RGB → matrix transform → sRGB, ensuring physically accurate results.

| Type | Affected cones | Common colors confused | |------|---------------|----------------------| | protanopia | Red (L) | Red-green, blue-violet | | deuteranopia | Green (M) | Red-green, rose-green | | tritanopia | Blue (S) | Blue-yellow, green-cyan |

Color manipulation

Everything is available through the immutable, lazily-evaluated Color class. The hex string is parsed at most once, and the expensive CAM16 forward pass and inverse solve run only when the result is actually needed — so reading channels repeatedly, or chaining edits without materializing, stays cheap.

All operations are available both as static methods (operating on a string or number) and as instance methods (operating on a Color):

import { Color } from "@panmdaa/colors";

// Constructors
const c = Color.from("#744c9d");   // hex string
Color.from(0x744c9d);              // ARGB integer
Color.fromHex("#744c9d");
Color.fromArgb(0x744c9d);
Color.fromHct(283, 36, 62);        // deferred solve — HCT values read instantly

// Read channels (computed once, then cached)
c.hue;      // 283
c.chroma;   // 36
c.tone;     // 62
Color.getHue("#744c9d");     // 283
Color.getChroma("#744c9d");  // 36
Color.getTone("#744c9d");    // 62

// Mutations return new Colors; the receiver never changes
c.lighten(10).saturate(20).rotateHue(45);  // chain freely
c.setHue(200).setChroma(50).setTone(80);
c.edit({ hue: 200, chroma: 40, tone: 70 });
Color.lighten("#744c9d", 10);     // +10 tone
Color.darken("#744c9d", 10);      // -10 tone
Color.saturate("#744c9d", 20);    // +20 chroma
Color.desaturate("#744c9d", 20);  // -20 chroma
Color.rotateHue("#744c9d", 90);   // +90 hue

// Foreground / background contrast pairing
Color.onColor("#000000");       // foreground for dark background
Color.underColor("#ffffff");    // background for light foreground

// Tone helpers
c.atTone(90);         // same hue/chroma, tone 90
c.tones();            // all 14 reference tones
Color.tone("#744c9d", 90);  // same hue/chroma, tone 90
const ts = Color.tones("#744c9d");
ts[50]; // tone 50 at source color's hue/chroma

// Conversions
c.toHexColor();       // "#744c9d"
c.toHex();            // alias of toHexColor
c.toArgb();           // ARGB integer
c.toNumber();         // alias of toArgb
c.toRgb();            // { r: 116, g: 76, b: 157 }
c.toHct();            // Hct instance

// Extras
c.contrastWith("#ffffff");
c.harmonize("#ff0000");
c.fixDisliked();      // c.isDisliked() check included

Color blending

import { Color } from "@panmdaa/colors";

// Blend two colors — perceptual midpoint in HCT space
Color.mix("#ff0000", "#0000ff");  // hue ~283 (purple)

// Blend any number of colors
Color.mix("#ff0000", "#00ff00", "#0000ff");  // three-way blend

// All inputs weighted equally, hue is circular-averaged
Color.mix("#ff0000", "#ff0000", "#0000ff");
// ≈ Color.mix("#ff0000", "#0000ff") with extra red weight

mix operates in HCT space — hue is circular-averaged (handles the 0°/360° wrap), chroma and tone are arithmetically averaged. The result is perceptually uniform, unlike naive RGB blending.

Color correction

import { Color } from "@panmdaa/colors";

// Harmonize a color to complement another
Color.harmonize("#ff0000", "#744c9d");  // shifts design color toward source

// Fix disliked colors (yellow-green, etc.)
Color.isDisliked("#4a7a3f");  // true
Color.fixDisliked("#4a7a3f"); // shifted to avoid the disliked zone

Advanced: HCT color space

import { Color } from "@panmdaa/colors";

const color = Color.hct("#744c9d");
color.hue;    // 283
color.chroma; // 36
color.tone;   // 62

// Create a color from HCT values
Color.fromHct(283, 36, 80).hex;  // "#c9aae0" (same hue/chroma, tone 80)

// Convert between formats
Color.toNumber("#744c9d");   // 7629981 (ARGB int)
Color.fromNumber(7629981);   // "#744c9d"

From images

import { Color } from "@panmdaa/colors";

const seed = await Color.fromImage(imageElement); // extracts dominant color
const theme = palette(seed, { variant: "expressive" });

API

| Function | Description | |----------|-------------| | palette(color, options?) | Generate light + dark theme (variant, extraColors, gradients) | | generateCSS(theme, options?) | CSS custom properties string | | generateCSSSheet(theme, options?) | Full stylesheet with light/dark blocks | | report(theme, mode?) | WCAG contrast report for all on-* pairs | | contrastChecker(base, on, cvd?) | Contrast score 0–10 with optional CVD simulation | | paletteChecker(theme/palette, cvd?) | Check all on-* pairs with scores, WCAG flags, and CVD | | simulateCVD(color, deficiency, severity?) | Simulate color under protanopia/deuteranopia/tritanopia | | simulateAllCVD(color, severity?) | Simulate color under all three deficiencies at once | | Color.from(hex/number) | Parse a hex string or ARGB integer | | Color.fromHex / fromArgb / fromHct / fromNumber / fromImage | Other constructors | | Color.getHue / getChroma / getTone | Read a single HCT channel | | Color.setHue / setChroma / setTone | Set a single HCT channel | | Color.lighten / darken | Adjust tone | | Color.saturate / desaturate | Adjust chroma | | Color.rotateHue | Rotate hue | | Color.edit(color, { hue?, chroma?, tone? }) | Batch channel edit | | Color.tone(color, tone) | Get color at a specific tone | | Color.tones(color) | All 14 reference tones at once | | Color.onColor / underColor | Contrast-guaranteed foreground/background | | Color.mix(...colors) | Blend N colors together in HCT space | | Color.gradient(from, to, count) | HCT-interpolated steps between two colors | | Color.harmonize / fixDisliked / isDisliked | Correction utilities | | Color.toNumber / toArgb / toRgb / toHct | Conversion methods on an instance |

Origins

@panmdaa/colors originated from Google's Material Color Utilities project and preserves its underlying color science (HCT, CAM16, dynamic color algorithms, quantization, etc.).

Over time, the implementation has been substantially refactored and evolved. Legacy compatibility layers, version-specific branches, and internal abstractions were removed in favor of a unified architecture with a stable, developer-oriented API.

Today, @panmdaa/colors is developed independently as part of the Panmdaa ecosystem while remaining compatible with the Material Design color model where appropriate.

Key differentiators:

  • @panmdaa/colors is the only library that combines HCT color science, full design system theme generation, custom token extensibility, contrast scores with CVD simulation, palette-wide accessibility checks, and image quantization in a single tree-shakeable zero-dependency package.
  • Material Color Utilities is Google's reference implementation. Its API is designed for internal Material Design usage and lacks ergonomic utilities like palette(), onColor(), report(), or CSS string generation.
  • Culori and Chroma.js are general-purpose color manipulation libraries with excellent interpolation, but they don't generate design-system themes from a seed color.
  • Radix Colors provides well-crafted light/dark scales for UI but doesn't handle HCT, dynamic theme generation, or programmatic color science.

Internal architecture

src/
├── hct/         ← HCT color space (CAM16, viewing conditions)
├── palette/     ← TonalPalette (hue + chroma → tones)
├── scheme/      ← DynamicScheme, DynamicColor, variants
├── spec/        ← Token definitions, palette specs, color calculation
├── science/     ← Blend, dislike analyzer, color blindness, temperature, score
├── quantize/    ← Image quantization (Wu, Celebi)
└── utils/       ← Color/math/string utilities

Built on proven color science, spec version 2026.

Scripts

| npm run | Description | |-----------|-------------| | build | Bundle with tsup (ESM + DTS) | | test | Run 80+ color correctness tests | | typecheck | TypeScript strict check | | lint | Biome lint | | format | Biome format |