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

@noctua-world/colors

v0.2.1

Published

A generated colour system: CSS custom properties, a Tailwind v4 theme, DTCG 2025.10 tokens and SCSS. Solved in OKLCH against APCA contrast targets, not hand-picked.

Readme

noctua-colors

A colour system whose colours were solved, not picked.

39 palettes · 1,767 semantic names · light and dark from one file · 48,441 contrast pairs checked on every build

Browse the palettes · Install · How it was made


Most colour systems are a list of hex values somebody chose. This one is the output of a compiler. You describe a hue; a program solves every step of every ramp against a perceptual contrast target, in every palette, and checks all 48,441 resulting pairs before anything ships.

You do not have to care about any of that to use it. You link one file.

<link rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/@noctua-world/colors/system/css/palette/blue-vivid.css">

That is a complete install. You now have --nc-color-surface, --nc-color-fg, --nc-color-accent, --nc-color-danger and 1,763 more — and dark mode already works.


Contents


Install in one minute

Pick the row that matches you. Each one is complete — there is no step two.

I just want colours in a web page

Copy this into your <head>:

<link rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/@noctua-world/colors/system/css/palette/blue-vivid.css">

Swap blue-vivid for any palette in the grid below.

29 KB gzipped, and that is one palette complete: the neutral ramp, every semantic name, light and dark. There is also a file carrying all 39, for switching at runtime — it is 741 KB gzipped, or 25× more, which is exactly why the one-palette file exists.

I use npm

npm install @noctua-world/colors

Then in your CSS:

@import "@noctua-world/colors/palette/blue-vivid.css";

I use Tailwind v4

npm install @noctua-world/colors

Two lines in your entry CSS, and that is the whole integration:

@import "tailwindcss";
@import "@noctua-world/colors/tailwind/palette/blue-vivid.css";

You now write bg-surface, text-fg, border-border, bg-accent, text-danger, bg-gray-18 — and dark: works, following the same signals the tokens do.

I write Rust

cargo add noctua-colors-tokens --features blue_vivid
use noctua_colors_tokens::blue_vivid::light::accent;

let hex = accent::SOLID.hex();      // a &'static str
let rgb = accent::SOLID.packed();   // a u32

no_std, no dependencies, every value a const.


Use it

You name meanings, not colours

.card {
  background: var(--nc-color-surface-raised);
  color:      var(--nc-color-fg);
  border:     1px solid var(--nc-color-border);
}

.card:focus-visible {
  outline: 2px solid var(--nc-color-ring);
}

.badge-overdue {
  background: var(--nc-color-overdue);
  color:      var(--nc-color-on-overdue);
}

Nothing there names a colour. Change the palette and all of it follows, because --nc-color-overdue is a name for a meaning, not for a particular red.

A complete page you can copy

Save this as index.html, open it, then switch your operating system between light and dark appearance.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>noctua-colors</title>
  <link rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/@noctua-world/colors/system/css/palette/jade-balanced.css">
  <style>
    body {
      background: var(--nc-color-surface);
      color: var(--nc-color-fg);
      font-family: system-ui, sans-serif;
      margin: 0;
      padding: 3rem 1.5rem;
    }
    .card {
      max-width: 32rem;
      margin-inline: auto;
      background: var(--nc-color-surface-raised);
      border: 1px solid var(--nc-color-border);
      border-radius: 12px;
      padding: 1.5rem;
    }
    .muted { color: var(--nc-color-fg-muted); }
    /* A solid fill takes on-<name>, which was solved against it. */
    .pill {
      display: inline-block;
      padding: 0.2rem 0.7rem;
      border-radius: 999px;
      background: var(--nc-color-success);
      color: var(--nc-color-on-success);
      font-size: 0.85rem;
    }
    /* A tinted background is a different pairing: -bg with -border, and the
       page's ordinary text colour on top. */
    .note {
      margin-top: 1rem;
      padding: 0.75rem 1rem;
      border-radius: 8px;
      background: var(--nc-color-info-bg);
      border: 1px solid var(--nc-color-info-border);
    }
    button {
      background: var(--nc-color-accent);
      color: var(--nc-color-on-accent);
      border: 0;
      border-radius: 8px;
      padding: 0.6rem 1.1rem;
      font: inherit;
      cursor: pointer;
    }
    button:hover { background: var(--nc-color-accent-hover); }
  </style>
</head>
<body>
  <div class="card">
    <span class="pill">Deployed</span>
    <h1>Dark mode already works</h1>
    <p class="muted">
      One stylesheet, both modes, nothing configured.
    </p>
    <button>Primary action</button>
    <div class="note">A tinted panel, using -bg with -border.</div>
  </div>
</body>
</html>

Choose a palette

Two axes — an accent hue and a saturation — so a palette name is always <accent>-<saturation>:

| | balanced | vivid | sober | |---|---|---|---| | ochre | ochre-balanced | ochre-vivid | ochre-sober | | amber | amber-balanced | amber-vivid | amber-sober | | lime | lime-balanced | lime-vivid | lime-sober | | jade | jade-balanced | jade-vivid | jade-sober | | teal | teal-balanced | teal-vivid | teal-sober | | azure | azure-balanced | azure-vivid | azure-sober | | blue | blue-balanced | blue-vivid | blue-sober | | indigo | indigo-balanced | indigo-vivid | indigo-sober | | violet | violet-balanced | violet-vivid | violet-sober | | magenta | magenta-balanced | magenta-vivid | magenta-sober | | rose | rose-balanced | rose-vivid | rose-sober | | clay | clay-balanced | clay-vivid | clay-sober | | umber | umber-balanced | umber-vivid | umber-sober |

ochre-balanced is the default — the one bound to :root in index.css, and what you get if you do not choose.

The saturation axis is a single multiplier on relative chroma. That is why it is an axis at all, rather than 39 separately tuned palettes. See them side by side.

Switching at runtime

To let your users choose, load the all-palettes file and set an attribute:

<link rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/@noctua-world/colors/system/css/index.css">
document.documentElement.dataset.palette = "violet-sober";

This is the 741 KB-gzipped route. If you ship one palette, do not use it.


The names you get

Every name below exists in every palette, in both modes.

Page furniture

| Name | For | |---|---| | --nc-color-surface | the page background | | --nc-color-surface-subtle | a slightly recessed area | | --nc-color-surface-raised | cards, panels, popovers | | --nc-color-fg | body text | | --nc-color-fg-muted | secondary text | | --nc-color-border | ordinary dividers and outlines | | --nc-color-border-strong | emphasised borders | | --nc-color-ring | focus rings |

States and meanings

The contract carries 1,767 names, covering 349 subjects. Each subject gives you a family:

| Pattern | Example | For | |---|---|---| | --nc-color-<subject> | --nc-color-danger | a solid fill | | --nc-color-<subject>-hover | --nc-color-danger-hover | its hover state | | --nc-color-<subject>-bg | --nc-color-danger-bg | a tinted background | | --nc-color-<subject>-border | --nc-color-danger-border | a border to match that background | | --nc-color-on-<subject> | --nc-color-on-danger | text that is legible on the solid fill |

--nc-color-on-danger was solved against --nc-color-danger to a contrast target, so you never guess whether white or black goes on top.

They come in two pairs, and mixing them is the one easy mistake:

/* A solid chip — the fill and the text solved against each other. */
.chip { background: var(--nc-color-danger); color: var(--nc-color-on-danger); }

/* A tinted panel — the background and its border. Ordinary page text on top. */
.panel { background: var(--nc-color-danger-bg);
         border: 1px solid var(--nc-color-danger-border); }

on-danger is a near-white, chosen to sit on the saturated fill. On the pale -bg it would be nearly invisible — and since both are valid colours, nothing would warn you.

A sample of the subjects:

accent  danger  warning  success  info  urgent  waiting  active  progress
pending  approved  rejected  archived  draft  published  deployed  failed
expired  overdue  locked  verified  deprecated  offline  syncing  throttled
quarantined  chargeback  backordered  onboarding  ratelimited  …

Ordinary product vocabulary — the point is that you rarely have to invent one. The complete list is in system/css/contexts.css and on the documentation site.

The neutral ramp

--nc-gray-1 through --nc-gray-24, plus --nc-gray-cool-* and --nc-gray-warm-*, for when the semantic names are not granular enough.

These are mode-independent: --nc-gray-4 is one colour, not two. The ramp is a shared resource both modes draw from.

Translucency

--nc-neutral-a1-a12 and --nc-accent-a1-a12 — washes built with color-mix, so they follow whichever mode and gamut layer is active rather than freezing a value.


Light and dark

It already works. All three mechanisms are emitted, composed so they cannot conflict:

| You do | It does | |---|---| | nothing | follows the operating system's preference | | <html data-theme="dark"> | forces dark | | <html class="dark"> | forces dark | | either, on any element | switches that subtree only |

So a toggle is one line:

document.documentElement.dataset.theme = wantsDark ? "dark" : "light";

color-scheme is set too, so scrollbars, form controls and the canvas match. Without it a dark page still gets light scrollbars.


Why these colours are trustworthy

Two ideas do the work. Both are covered at full depth in docs/COMPILER.md — here is what they buy you.

Chroma is relative, not absolute. Saturation is stored as a fraction of the most a display can actually show at that lightness and hue. So the same token is richer on a wide-gamut screen without being redefined anywhere, and a yellow and a blue look equally saturated at the same step despite blue having nearly twice the chroma available.

Lightness is solved, not authored. Every step's lightness is derived from a contrast target, using APCA rather than WCAG 2.x. That distinction is not academic:

| pair | WCAG 2.x | APCA | |---|---|---| | #767676 on #ffffff | 4.54:1 — barely AA | Lc +71.6 — comfortable | | #9a9a9a on #000000 | 7.46:1 — passes AAA | Lc −47.7 — too weak for body text |

WCAG rates the dark-mode pair as substantially better. APCA, which models polarity and the actual perceptual response, rates it markedly worse — which matches what your eyes report. A system tuned to WCAG ships both as equivalent. WCAG ratios are still emitted, as a compliance report; no gate targets them.

And it is all checked. Every build runs 48,441 checks across every palette, mode and pair, and fails on a regression. Colour-vision simulation runs too — and where a pair genuinely cannot be made distinguishable, that is published rather than hidden, in system/reports/colour-vision.md, with the measured numbers. Twelve chart series cannot all be told apart under protanopia; no palette can fix that, so the honest answer is a legend, and the report says so.


Every way to install

| Route | Command or line | |---|---| | CDN, one palette | <link href="https://cdn.jsdelivr.net/npm/@noctua-world/colors/system/css/palette/blue-vivid.css"> | | CDN, all palettes | <link href="https://cdn.jsdelivr.net/npm/@noctua-world/colors/system/css/index.css"> | | npm | npm install @noctua-world/colors | | Tailwind v4 | @import "@noctua-world/colors/tailwind/palette/blue-vivid.css"; | | Rust | cargo add noctua-colors-tokens --features blue_vivid | | Cargo, from git | noctua-colors-tokens = { git = "https://github.com/noctua-world/noctua-colors", tag = "v0.2.0" } | | SCSS | @use "@noctua-world/colors/scss/noctua" as noctua; | | Design tokens (DTCG) | @noctua-world/colors/tokens/blue-vivid-light.json | | TypeScript / JSON | in the release tarball — npm leaves out the 19 MB payload | | Qt / Quickshell | copy system/qml/ from a release | | Nix | inputs.noctua-colors.url = "github:noctua-world/noctua-colors"; | | Submodule, subtree, plain copy | system/ is committed; take what you want |

Plain CSS, from a checkout or a copy

system/ is committed, so there is no build step:

<link rel="stylesheet" href="system/css/palette/ochre-balanced.css">

Or the three layers separately, if you want to swap only the palette:

<link rel="stylesheet" href="system/css/ramp.css">      <!-- the dense grays -->
<link rel="stylesheet" href="system/css/contexts.css">  <!-- the contract -->
<link rel="stylesheet" href="system/css/ochre-balanced.css">  <!-- the values -->

Link all three, or none. A theme file on its own defines no --nc-gray-* and no --nc-color-*, and CSS drops an undefined custom property without saying so — the page renders unstyled and the console stays empty. The per-palette file exists so that you cannot make this mistake.

npm subpaths

| Subpath | Contents | |---|---| | @noctua-world/colors | all palettes (css/index.css) | | @noctua-world/colors/palette/<name>.css | one palette, self-contained | | @noctua-world/colors/css/<name>.css | one theme's values only | | @noctua-world/colors/tailwind | Tailwind, all palettes | | @noctua-world/colors/tailwind/palette/<name>.css | Tailwind, one palette | | @noctua-world/colors/scss/noctua | the SCSS entry | | @noctua-world/colors/tokens/<name>-<mode>.json | DTCG tokens | | @noctua-world/colors/axes.json | the accent × saturation grid | | @noctua-world/colors/manifest.json | hashes and both versions |

Tailwind v4, all palettes

@import "tailwindcss";
@import "@noctua-world/colors/tailwind";

Utilities come out as var() references rather than frozen values, so they follow the active mode. That is @theme inline, and it is load-bearing: a plain @theme would stamp bg-accent with whichever mode was active at build time, and dark mode would silently stop working.

Rust

[dependencies]
noctua-colors-tokens = { version = "0.2.0", features = ["blue_vivid"] }

One Cargo feature per palette, so a program shipping two does not carry the other 37.

use noctua_colors_tokens::blue_vivid::{dark, light};

const CARD: &str = light::neutral::BG_ELEMENT.hex();
let packed: u32 = dark::accent::SOLID.packed();
let (l, c, h) = light::accent::SOLID.oklch();

Design tokens (DTCG 2025.10)

system/tokens/<palette>-<mode>.json is standards-compliant DTCG. Colour values are objects with colorSpace: "oklch" and components, plus a 6-digit hex fallback — which is what the 2025.10 stable specification requires, and what tools like Style Dictionary v5 actually read.

// style-dictionary config
export default {
  source: ["node_modules/@noctua-world/colors/system/tokens/blue-vivid-light.json"],
  platforms: {
    css: {
      transformGroup: "css",
      files: [{ destination: "vars.css", format: "css/variables" }],
    },
  },
};

SCSS

@use "@noctua-world/colors/scss/noctua" as noctua;

.card { background: noctua.$nc-blue-vivid-light-neutral-bg-element; }

Values are hex, because Sass resolves at compile time and cannot follow a var(). Both modes are emitted; you pick one.

TypeScript and JSON

system/json/palette.json is every theme, mode, family, step, role, gamut rendition and relative chroma as plain JSON. system/ts/index.js is the same data with .d.ts types.

Both are ~19 MB and are deliberately excluded from the npm package — they are in the release tarball instead, because almost nobody installing colours wants to download them.

Qt / QML

Copy system/qml/ next to your QML, then:

import "qml"

Rectangle {
    color: BlueVividDark.surface
    border.color: BlueVividDark.border
}

Nix

{
  inputs.noctua-colors.url = "github:noctua-world/noctua-colors";

  # then, in a derivation:
  installPhase = ''
    mkdir -p $out/static
    cp -r ${noctua-colors}/share/noctua-colors/css $out/static/
  '';
}

Verifying what you installed

Every release is published from CI with no stored credentials, over OIDC, and carries signed build provenance.

# npm
npm audit signatures

# release assets
gh attestation verify noctua-colors-v0.2.0-system.tar.gz \
  --repo noctua-world/noctua-colors
sha256sum -c SHA256SUMS

# any file, against the manifest
b3sum system/css/index.css   # compare with system/MANIFEST.json

Two versions, and which one is yours

This repository publishes two things, on two clocks:

| | What it is | Where you see it | |---|---|---| | The colour system | the palettes, the names, the values | npm, crates.io, the git tag | | The compiler | the program that generated them | MANIFEST.json's version |

The colour system's version is the one that matters to you. A tag names it, both registries publish it, and TOKEN-POLICY.md says exactly what a bump may and may not change. The compiler's version is bookkeeping: it is published nowhere, and a refactor that touches no colour does not move the number you depend on.


Documentation

| Document | For | |---|---| | The palette browser | seeing every palette, live | | docs/COMPILER.md | how this was made — the two ideas at depth, the gamut-boundary derivation, and what the quality gates found but could not fix | | TOKEN-POLICY.md | what you may depend on, and what a version bump means | | CHANGELOG.md | which colour changed, and whether your contrast still holds | | CONTRIBUTING.md | working on this repository | | SECURITY.md | supply-chain posture, and how to report an issue | | AGENTS.md | the operating manual: invariants, commands, and the gotchas that have already cost debugging time here | | specs/noctua.toml | the specification — heavily commented, and the only file a developer edits | | examples/ | a Rust consumer and a web consumer, both built by CI |


Licence

MIT OR Apache-2.0, at your option. See LICENSE-MIT and LICENSE-APACHE.