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

@crob/color-pair

v0.1.0

Published

Adaptive foreground/background color pairs (light + dark) from Tailwind colors or any hex, via OKLCH.

Readme

@crob/color-pair

Adaptive foreground/background color pairs (light + dark) from Tailwind colors or any hex, via OKLCH.

npm version CI license types zero dependencies

Why

Badges, tags, status pills, and icon chips all need the same thing: a foreground color that reads cleanly over a tinted background, in both light and dark mode. Doing this by hand for every color is tedious, and doing it consistently across an app is harder. color-pair produces that pairing for you from either a named Tailwind color or any hex value, and returns a single, uniform shape so your UI never has to branch on where the color came from. It is pure math with zero runtime dependencies, so it runs the same in Node and the browser.

Install

npm i @crob/color-pair
pnpm add @crob/color-pair
yarn add @crob/color-pair

Quick start

There are two ways to ask for a pair. Both return the identical shape.

import { buildTailwindColorPair, buildColorPair } from '@crob/color-pair'

// 1. Exact Tailwind v4 utility classes for a named palette color.
buildTailwindColorPair('red')
// {
//   light: {
//     foreground: { class: 'text-red-700', hex: '#c10007' },
//     background: { class: 'bg-red-50',  hex: '#fef2f2' },
//   },
//   dark: {
//     foreground: { class: 'text-red-400',    hex: '#ff6467' },
//     background: { class: 'bg-red-400/10',    hex: '#ff64671a' },
//   },
// }

// 2. A Tailwind-shaped pair derived from any hex (arbitrary-value classes).
buildColorPair('#7c5cff')
// {
//   light: {
//     foreground: { class: 'text-[#644acf]', hex: '#644acf' },
//     background: { class: 'bg-[#f6f6ff]',  hex: '#f6f6ff' },
//   },
//   dark: {
//     foreground: { class: 'text-[#a696ff]',  hex: '#a696ff' },
//     background: { class: 'bg-[#a696ff]/10', hex: '#a696ff1a' },
//   },
// }

Each token carries both a ready-to-use class string and the resolved hex, so you can apply whichever fits your styling approach.

Prefer an instance when you want to reuse configuration:

import { ColorPairService } from '@crob/color-pair'

const colors = new ColorPairService({ darkBgAlpha: 0.12 })
colors.buildFromTailwind('blue')
colors.buildFromHex('#7c5cff')

Using the result

Each ColorToken gives you two ways to apply the color. The hex is always safe; the class needs a Tailwind build that can see it (see the safelist caveat).

Framework-neutral, via hex (always safe):

const pair = buildColorPair('#7c5cff')
element.style.color = pair.light.foreground.hex
element.style.backgroundColor = pair.light.background.hex

Vue, via class:

<script setup lang="ts">
import { buildTailwindColorPair } from '@crob/color-pair'
const pair = buildTailwindColorPair('emerald')
</script>

<template>
  <span :class="[pair.light.foreground.class, pair.light.background.class]">Active</span>
</template>

React, via hex (safest for dynamic/stored colors):

import { buildColorPair } from '@crob/color-pair'

function Badge({ color, children }: { color: string; children: React.ReactNode }) {
  const { light } = buildColorPair(color)
  return (
    <span style={{ color: light.foreground.hex, backgroundColor: light.background.hex }}>
      {children}
    </span>
  )
}

API reference

buildColorPair(hex, overrides?) => AdaptiveColorPair

Derive a pair from any hex using the default config. Emits arbitrary-value classes (text-[#…]). Throws InvalidHexColorError on malformed input.

buildTailwindColorPair(name, overrides?) => AdaptiveColorPair

Build a pair from a named Tailwind color using the default config. Emits exact utility classes (text-red-700). Throws UnknownTailwindColorError for an unknown name.

class ColorPairService

new ColorPairService(overrides?: ColorPairOverrides)
  • buildFromTailwind(name, overrides?) => AdaptiveColorPair — exact Tailwind v4 classes for a family (or black / white). Throws UnknownTailwindColorError for unknown names.
  • buildFromHex(hex, overrides?) => AdaptiveColorPair — derives an arbitrary-value pair from a hex. Throws InvalidHexColorError for malformed hex.
  • deriveShades(hex, overrides?) => { background, mid, foreground } — the three raw derived hex shades, with no class assembly. Useful when you only need the colors.

Pure helpers

  • normalizeHex(input) => string — expands #rgb shorthand, lowercases, returns #rrggbb. Throws InvalidHexColorError otherwise.
  • hexToOklch(hex) => Oklch — convert a hex color to { L, C, H }.
  • oklchToHex(L, C, H) => string — convert OKLCH back to a 6-digit hex, clamping out-of-gamut channels into sRGB.

Constants

  • TAILWIND_PALETTE — 28 PaletteFamily entries: 26 chromatic/neutral families (11 hexes each) followed by black and white (each a single swatch, hexes: []).
  • TAILWIND_STEPS['50', '100', …, '950'].
  • TAILWIND_COLOR_NAMES — every selectable name (the source of the TailwindColorName type).
  • DEFAULT_COLOR_PAIR_CONFIG — the default anchors, referenceChroma, and darkBgAlpha.

Errors

  • InvalidHexColorError (name === 'InvalidHexColorError') — thrown for a malformed hex.
  • UnknownTailwindColorError (name === 'UnknownTailwindColorError') — thrown for an unknown Tailwind name.

How derivation works

The hex path treats your input as hue only. Lightness and chroma come from three fixed anchors, and the input's chroma is used solely to scale the anchors' chroma so muted picks stay muted and grays stay gray.

  1. Convert the input hex to OKLCH and read its hue H and chroma C.
  2. Compute vividness = min(1, C / referenceChroma).
  3. For each anchor, derive a hex at oklch(anchor.lightness, anchor.chroma * vividness, H).

The anchors and their roles:

| Anchor | ~Tailwind step | Used for | | ------------ | -------------- | --------------------------------- | | background | 50 | light-mode background | | mid | 400 | dark-mode foreground and dark-mode background (with alpha) | | foreground | 700 | light-mode foreground |

The named path (buildFromTailwind) skips the math entirely and reads the exact Tailwind hexes at steps 700 / 50 / 400, assembling the matching utility classes.

Configuration

interface ColorPairConfig {
  anchors: {
    background: { lightness: number; chroma: number }
    mid: { lightness: number; chroma: number }
    foreground: { lightness: number; chroma: number }
  }
  referenceChroma: number
  darkBgAlpha: number
}

Defaults (DEFAULT_COLOR_PAIR_CONFIG):

{
  anchors: {
    background: { lightness: 0.977, chroma: 0.016 }, // ~50
    mid:        { lightness: 0.746, chroma: 0.189 }, // ~400
    foreground: { lightness: 0.514, chroma: 0.195 }, // ~700
  },
  referenceChroma: 0.219,
  darkBgAlpha: 0.1,
}

Config can be set per instance (constructor) and overridden per call. Per-call overrides win, and anchors merge shallowly one level deep, so you can override just mid without redefining the rest:

const colors = new ColorPairService({ darkBgAlpha: 0.12 })

colors.buildFromHex('#7c5cff', {
  anchors: { mid: { lightness: 0.7, chroma: 0.2 } }, // background & foreground keep their defaults
  darkBgAlpha: 0.2, // beats the 0.12 from the constructor
})

darkBgAlpha drives both the class suffix (bg-…/{pct}) and the 8-digit hex alpha, so they always agree.

Palette reference

The palette is the 26 Tailwind v4 families plus black and white (28 entries total). The newer v4 colors (taupe, mauve, mist, olive) are included. For families, buildFromTailwind returns the exact Tailwind utility classes. black and white have no 50/400/700 steps, so they use a fixed neutral recipe.

TypeScript

The package is written in TypeScript and ships full type declarations. Every interface from the API is exported, along with the TailwindColorName union, so unknown names are caught at compile time. It is authored against strict, noUncheckedIndexedAccess, and exactOptionalPropertyTypes.

FAQ / caveats

Tailwind safelisting

The class strings from buildFromTailwind (text-red-700, bg-red-400/10, …) are real Tailwind utilities assembled at runtime. Tailwind's JIT only generates classes it can actually see in your source, so names built dynamically will not exist in the final CSS unless you either:

  • safelist the relevant families/steps in your Tailwind config,
  • ensure those literal classes appear somewhere scannable, or
  • simply bind the hex instead, which never touches the Tailwind build.

The buildFromHex arbitrary-value classes (text-[#…]) are self-describing and generally JIT-safe, but for stored or fully dynamic colors the universally safe path is the hex.

Runtime support

Pure math, no DOM or Node APIs. Works in any modern browser and in Node >=18.

No Tailwind dependency

The package emits Tailwind class strings but does not import or depend on Tailwind. There are no runtime dependencies and nothing in peerDependencies.

Contributing

This package is released with Changesets. When you make a change that should ship, add a changeset describing it:

npm run changeset

On every push to main, CI opens a Version Packages PR that bumps the version and updates CHANGELOG.md. Merging that PR tags the release, creates a GitHub release, and publishes to npm. The golden fixtures in test/fixtures.test.ts are the semver guardrail: any change to a fixture value is a breaking change.

Changelog

See CHANGELOG.md.

License

MIT © Robert-Cristian Chiribuc