@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.
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
