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

oklch-shades

v1.0.1

Published

Perceptually uniform neutral scale generation using OKLCH.

Downloads

180

Readme

oklch-shades

Perceptually uniform neutral scale generation using OKLCH.
WCAG 2.1 + APCA (WCAG 3.0) contrast auditing.
Zero dependencies. Works in Node, browser, Deno, and Figma plugins.

Author: Olawale Balo — Product Designer + Design Engineer


What it does

Takes any pure neutral hex scale and generates perceptually uniform warm or cool variants by:

  1. Converting each step to OKLCH
  2. Keeping L (lightness) identical — same perceived brightness
  3. Introducing a small, tapered chroma (C) at a fixed hue angle (H)
  4. Converting back to hex

Unlike RGB channel nudging, this method preserves perceptual uniformity across the full scale. The WCAG AA boundary stays locked.


Install

npm i oklch-shades

Or use locally in a monorepo:

{
  "dependencies": {
    "oklch-shades": "file:../oklch-shades"
  }
}

Usage

import { generateScale, auditScale, firstPassingStep, HUE } from 'oklch-shades'

const gray = {
  "0":   "#FFFFFF",
  "50":  "#FAFAFA",
  "100": "#F5F5F5",
  "200": "#E5E5E5",
  "300": "#D3D3D3",
  "400": "#A1A1A1",
  "500": "#757575",
  "600": "#5C5C5C",
  "700": "#3D3D3D",
  "800": "#262626",
  "900": "#1C1C1C",
  "950": "#0A0A0A",
}

// Generate warm (sand) and cool (slate) variants
const sand  = generateScale(gray, { hue: HUE.sand  })  // 68°  yellow-amber
const slate = generateScale(gray, { hue: HUE.slate })  // 255° blue-slate

// Audit a scale against white — OKLCH values + WCAG results per step
const audit = auditScale(slate)

// Find the first step that passes WCAG AA (4.5:1)
const boundary = firstPassingStep(slate)
// → { step: "500", hex: "#71767B", ratio: 4.59 }

Hue presets

import { HUE } from 'oklch-shades'

HUE.sand   // 68°  — yellow-amber warm
HUE.amber  // 68°
HUE.stone  // 75°  — slightly more orange-warm
HUE.rose   // 20°  — red-warm
HUE.slate  // 255° — blue-slate cool
HUE.sky    // 220° — lighter blue cool
HUE.teal   // 195° — teal cool
HUE.mauve  // 310° — purple-adjacent

Custom chroma

Control toning intensity per step:

const subtle = generateScale(gray, {
  hue: HUE.slate,
  chromaMap: {
    "500": 0.005,  // much quieter midtone
    "600": 0.005,
  }
})

const strong = generateScale(gray, {
  hue: HUE.sand,
  chromaMap: {
    "400": 0.018,  // push toward tinted
    "500": 0.020,
  }
})

Default chroma curve (tapers at extremes):

| Step | Chroma | |------|--------| | 0 | 0.000 | | 50 | 0.004 | | 100 | 0.004 | | 200 | 0.007 | | 300 | 0.007 | | 400 | 0.010 | | 500 | 0.010 | | 600 | 0.009 | | 700 | 0.009 | | 800 | 0.005 | | 900 | 0.005 | | 950 | 0.005 |


API

generateScale(pure, options)

Generate a toned neutral scale from a pure neutral.

| Param | Type | Description | |-------|------|-------------| | pure | NeutralScale | Source scale { "500": "#757575" } | | options.hue | number | OKLCH hue angle 0–360 | | options.chromaMap | object? | Override chroma per step |

Returns NeutralScale


auditScale(scale, background?)

Audit every step — returns OKLCH values and WCAG contrast results.

Returns ScaleAudit[]


firstPassingStep(scale, background?)

Find the first step that passes WCAG AA (4.5:1).

Returns { step, hex, ratio } | null


hexToOklch(hex) / oklchToHex(color)

Direct OKLCH ↔ hex conversion.


contrastAudit(foreground, background?)

Combined WCAG 2.1 + APCA audit in one call. Returns both results.

const result = contrastAudit('#71767B', '#FFFFFF')

// WCAG 2.1
result.wcag21.ratio          // 4.59
result.wcag21.passAA         // true
result.wcag21.passAAA        // false

// APCA (WCAG 3.0)
result.apca.lc               // 68.5  (signed — positive = dark on light)
result.apca.lcAbs            // 68.5
result.apca.passBodyText     // false — needs 75 Lc
result.apca.passLargeText    // true  — passes 60 Lc
result.apca.passUIElement    // true  — passes 45 Lc
result.apca.passPlaceholder  // true  — passes 30 Lc

apcaLc(foreground, background?)

Raw APCA Lc value. Signed — positive = dark text on light bg.

Implements APCA-W3 0.0.98G-4g (current Bronze/Simple mode spec).

apcaLc('#3A3D42', '#FFFFFF')  // → 91.5 Lc  — fluent body text
apcaLc('#71767B', '#FFFFFF')  // → 68.5 Lc  — large text
apcaLc('#9DA2A7', '#FFFFFF')  // → 47.8 Lc  — UI components
apcaLc('#FFFFFF', '#0A0A0A')  // → -104 Lc  — light on dark

APCA thresholds (0.0.98G-4g)

| Lc (abs) | Use case | |----------|----------| | 90+ | Fluent body text, long-form reading | | 75+ | Body text, normal weight columns | | 60+ | Large text 18px+ / subheadings | | 45+ | UI components, icons, non-text | | 30+ | Placeholder, disabled, decorative | | 15+ | Incidental / invisible-pass minimum |

Note: APCA is a draft standard. WCAG 2.1 remains the current legal requirement. Use both for forward-compatible, perceptually sound decisions.


apcaAudit(foreground, background?)

Full APCA audit with all threshold checks.


wcagAudit(foreground, background?)

Full WCAG audit — returns { ratio, passAA, passAALarge, passAAA }.


Use with Style Dictionary

// build-tokens.js
import { generateScale, HUE } from 'oklch-shades'
import StyleDictionary from 'style-dictionary'

const gray  = { /* your gray scale */ }
const slate = generateScale(gray, { hue: HUE.slate })

// Pass slate into your token definition
const tokens = {
  color: {
    neutral: Object.fromEntries(
      Object.entries(slate).map(([step, hex]) => [
        step,
        { value: hex, type: 'color' }
      ])
    )
  }
}

Output sample

gray  neutral-0     #FFFFFF
gray  neutral-50    #FAFAFA
gray  neutral-500   #757575   4.61:1 ✓

sand  neutral-0     #FFFFFF
sand  neutral-50    #FCFAF7
sand  neutral-500   #79746F   4.62:1 ✓

slate neutral-0     #FFFFFF
slate neutral-50    #F8FAFD
slate neutral-500   #71767B   4.59:1 ✓

License

MIT