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 🙏

© 2025 – Pkg Stats / Ryan Hefner

dittotones

v0.6.0

Published

Transform any color into a full palette by copying the perceptual DNA of Tailwind, Radix, and other design systems

Readme

dittoTones 🟣

A mini-library to transform any color into a full palette, based on the perceptual "DNA" of any design system.

Demo: https://meodai.github.io/dittoTones/

How it works

Most palette generators for popular frameworks either match a single color or ignore the careful work that was put into creating the original palettes entirely. dittoTones takes a different approach: it analyzes the perceptual "DNA" (Lightness and Chroma curves in Oklch space) of popular design systems like Tailwind or Radix. It then maps your target hue onto these curves, ensuring your custom palette maintains similar accessible contrast ratios and vibrancy as the reference system.

Install

npm install dittotones

Usage

import { DittoTones } from 'dittotones';
import { tailwindRamps } from 'dittotones/ramps/tailwind';
// or
import { radixRamps } from 'dittotones/ramps/radix';
import { formatCss, formatHex } from 'culori';

// Use Tailwind ramps (shades: 50-950)
const ditto = new DittoTones({ ramps: tailwindRamps });

// Or Radix ramps (shades: 1-12)
const dittoRadix = new DittoTones({ ramps: radixRamps });

const result = ditto.generate('#F97316');

// result.scale contains Oklch color objects
// Use culori's formatCss or formatHex to convert:

for (const [shade, color] of Object.entries(result.scale)) {
  console.log(`${shade}: ${formatCss(color)}`);
  // 50: oklch(0.98 0.016 49)
  // 100: oklch(0.954 0.038 49)
  // ...
}

// Or as hex:
for (const [shade, color] of Object.entries(result.scale)) {
  console.log(`${shade}: ${formatHex(color)}`);
}

Result

interface GenerateResult {
  inputColor: Oklch; // Parsed input color
  matchedShade: string; // e.g. "500"
  method: 'exact' | 'single' | 'blend';
  sources: {
    // Which ramps were used
    name: string;
    diff: number;
    weight: number;
  }[];
  scale: Record<string, Oklch>; // The generated palette
}

How it works

  1. Parse input — converts the input into Oklch via culori
  2. Handle neutrals — if chroma is very low, picks the “most neutral” ramp and returns it as-is
  3. Find closest match — finds the nearest ramp color by Euclidean distance in OKLCH (diff)
  4. Pick strategyexact if diff is below a small threshold, otherwise single (one ramp) or blend (two ramps; second ramp chosen by closest hue at the matched shade)
  5. Rotate hue + correct L/C — sets the target hue across the scale, then offsets lightness and scales chroma so the matched shade lands on the input color

Custom ramps

import { DittoTones } from 'dittotones';
import { parse, oklch, type Oklch } from 'culori';

const customRamps = new Map([
  [
    'brand',
    {
      '50': oklch(parse('oklch(98% 0.01 250)')) as Oklch,
      '500': oklch(parse('#3B82F6')) as Oklch,
      '950': oklch(parse('oklch(25% 0.05 250)')) as Oklch,
    },
  ],
]);

const ditto = new DittoTones({ ramps: customRamps });

Dev

npm install
npm run dev     # Start dev server with demo
npm run build   # Build library
npm run preview # Preview the demo build

Notes

  • ESM-only package ("type": "module").

Flowchart

      Input Color
           │
           ▼
     Parse to OKLCH
           │
           ▼
   Is chroma very low?
     ┌─────┴─────┐
     ▼           ▼
    yes          no
     │           │
     ▼           ▼
 Use most     Find closest ramp
 neutral      + matched shade
 ramp             │
     │            │
     │            ▼
     │   Is diff below threshold?
     │       ┌────┴────┐
     │       ▼         ▼
     │      yes        no
     │       │         │
     │       ▼         ▼
     │  Use single   Pick second ramp
     │     ramp      (closest hue at
     │               matched shade)
     │                 │
     │           ┌─────┴─────┐
     │           ▼           ▼
     │          none       found
     │           │           │
     │           ▼           ▼
     │      Use single   Blend ramps
     │         ramp      (weighted)
     │           │           │
     └──────┬────┴──────┬────┘
            │           │
            ▼           ▼
      Rotate hue + correct L/C
                 │
                 ▼
         Generated Palette

Credits

Built with Culori for color math and interpolation.

License

MIT