@didrod2539/okcolor
v0.2.0
Published
Tiny, zero-dependency color toolkit with modern OKLab/OKLCH color science and WCAG accessibility. Parse, convert, mix perceptually, and check contrast. Works in Node, Deno, Bun and the browser.
Downloads
258
Maintainers
Readme
okcolor
Modern color for the web — OKLab/OKLCH color science and WCAG accessibility in one tiny, zero-dependency package.
Color math is unforgiving: a "lighter" shade in naive HSL looks muddy, and a WCAG contrast ratio is a precise formula (sRGB linearization → relative luminance), not a vibe. okcolor does both correctly — it lightens and mixes in OKLab (perceptually uniform) and computes WCAG contrast exactly — in about 3 KB with zero dependencies.
import { color, contrast, readableTextColor } from "@didrod2539/okcolor";
color("#3498db").lighten(0.1).toHex(); // perceptual lightening, not HSL
color("red").mix("blue").toHex(); // "#8c53a2" — clean OKLab midpoint
contrast("#777", "#fff"); // 4.48 (exact WCAG ratio)
color("#767676").isReadable("#fff"); // true (meets AA)
readableTextColor("#3498db").toHex(); // "#000000" — auto pick black/whiteWhy okcolor?
- 🎨 OKLab/OKLCH built in.
lighten,darken,mix,grayscaleall operate in OKLab, so results look right — no muddy purples, no uneven lightness ramps. - ♿ WCAG accessibility, exact. Relative luminance, contrast ratio, and
isReadable(AA/AAA, normal/large) — the math accessibility audits actually use. - 🔁 Every format. Parse and emit hex (3/4/6/8),
rgb(),hsl(),oklch(), and all 148 CSS named colors. - 🧊 Immutable & chainable.
color("#abc").darken(0.1).saturate(0.2).alpha(0.8).toHslString(). - 🪶 ~3 KB gzipped, zero dependencies. Node 18+, Deno, Bun, Workers and the browser.
- 🛡️ Type-safe. Written in TypeScript, ships full declarations.
Install
npm install @didrod2539/okcolor
# or: pnpm add @didrod2539/okcolor / yarn add @didrod2539/okcolorPublished under the
@didrod2539npm scope (the unscoped nameokcolorwas blocked by npm for being too close tocolor). The import name matches the package name; everything else is identical.
Ships ESM and CommonJS:
import { color } from "@didrod2539/okcolor"; // ESM / TypeScript
const { color } = require("@didrod2539/okcolor"); // CommonJSCLI
npx @didrod2539/okcolor "#3b82f6" # all formats + a swatch
npx @didrod2539/okcolor convert rebeccapurple --to oklch
npx @didrod2539/okcolor contrast "#fff" "#777" # ratio + WCAG (exit 1 if AA fails)
npx @didrod2539/okcolor mix red blue # perceptual OKLab midpoint
npx @didrod2539/okcolor shades "#3b82f6" # a lighten→darken rampThe bundled command is okcolor. contrast exits non-zero when a pair
fails WCAG AA — drop it into CI to guard your color tokens.
Usage
Parse & convert
color("#3498db").toRgbString(); // "rgb(52, 152, 219)"
color("rgb(52 152 219)").toHsl(); // { h: 204, s: 70, l: 53, a: 1 }
color("rebeccapurple").toOklchString(); // "oklch(0.4422 0.1656 303.37)"
color("hsl(210 50% 50%)").toHex(); // "#4080bf"
color("oklch(0.7 0.15 250)").toHex(); // OKLCH in, hex outManipulate (perceptually)
const brand = color("#3498db");
brand.lighten(0.1); // +0.1 OKLCH lightness
brand.darken(0.1);
brand.saturate(0.2); // +20% chroma
brand.desaturate(0.2);
brand.rotate(180); // complementary hue
brand.grayscale(); // chroma → 0, lightness preserved
brand.mix("white", 0.25); // 25% toward white, in OKLab
brand.mix("white", 0.25, "srgb"); // ...or naive sRGB if you prefer
brand.alpha(0.5).toRgbString(); // "rgba(52, 152, 219, 0.5)"Accessibility (WCAG 2.1)
color("#fff").luminance(); // 1
contrast("#000", "#fff"); // 21
const fg = color("#767676");
fg.isReadable("#ffffff"); // true (AA, normal text)
fg.isReadable("#ffffff", { level: "AAA" }); // false
fg.isReadable("#ffffff", { size: "large" }); // true
// Auto-pick legible text for any background
readableTextColor("#3498db").toHex(); // "#000000"
readableTextColor("#1a1a1a").toHex(); // "#ffffff"API
| Member | Description |
| ------ | ----------- |
| color(input) | Parse a hex/rgb/hsl/oklch/named string, {r,g,b,a}, or Color. |
| oklch(l, c, h, a?) | Build a color from OKLCH coordinates. |
| .toHex() / .toHexa() / .toRgbString() / .toHslString() / .toOklchString() | Output formats. |
| .rgba / .toHsl() / .toOklch() | Structured values. |
| .lighten / .darken / .saturate / .desaturate / .rotate / .grayscale / .invert / .alpha | Manipulation (return new Color). |
| .mix(other, weight?, mode?) | Blend in "oklab" (default) or "srgb". |
| .luminance() / .contrast(other) / .isReadable(bg, opts?) / .isLight() / .isDark() | WCAG accessibility. |
| contrast(a, b) | Contrast ratio of two colors. |
| readableTextColor(bg) | Black or white — whichever is more legible. |
| isValid(str) | Does the string parse? |
Why OKLab?
HSL's "lightness" doesn't match human perception: yellow at 50% looks far
brighter than blue at 50%. OKLab (Björn Ottosson, 2020) is a
perceptually-uniform space, so equal lightness steps look equal and color
mixes pass through sensible midpoints instead of grey mud. okcolor implements
the full sRGB → linear → LMS → OKLab pipeline, so lighten, mix and
grayscale behave the way a designer expects.
Comparison
| | okcolor | naive hex/HSL helpers | larger color libs |
| ------------------------ | :-------: | :-------------------: | :---------------: |
| OKLab / OKLCH | ✅ | ❌ | ⚠️ |
| WCAG contrast & a11y | ✅ | ❌ | ⚠️ |
| Perceptual mix/lighten | ✅ | ❌ | ⚠️ |
| Zero dependencies | ✅ | ✅ | ⚠️ |
| ~3 KB gzipped | ✅ | ✅ | ❌ |
Contributing
Contributions are very welcome! Please read CONTRIBUTING.md and our Code of Conduct.
git clone https://github.com/didrod205/okcolor.git
cd okcolor
npm install
npm test💖 Sponsor
okcolor is free and MIT-licensed, built and maintained in spare time. If it
made your palettes prettier or your UI more accessible, please consider
supporting it — every bit helps keep the project healthy.
- ⭐ Star this repo — the simplest, free way to help others discover it.
- 🍋 Sponsor via Lemon Squeezy — one-time or recurring support.
Sponsoring? Open an issue and we'll add your name/logo here. Thank you! 🙏
License
MIT © okcolor contributors
