Skip to content

πŸ›Ό A TypeScript-first, OKLCH-based, framework-agnostic color library that validates color strings at compile time

License

Notifications You must be signed in to change notification settings

junhoyeo/use-color

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

hero

πŸ›Ό use-color

A TypeScript-first, OKLCH-based, framework-agnostic color library
that validates color strings at compile time.

Never ship an invalid color string again.

NPM version Bundle size TypeScript Coverage

Playground

Compile-time Type Checking

The only color library that catches invalid colors at compile time.

+ color('#00ffaa')
- color('#00ffzz')
// Argument of type '"#00ffzz"' is not assignable to parameter of type...

+ color('rgb(255, 128, 0)')
- color('rgb(255, 128,)')
// Argument of type '"rgb(255, 128,)"' is not assignable to parameter of type...

+ color('oklch(0.7 0.15 180)')
- color('oklch(invalid)')
// Argument of type '"oklch(invalid)"' is not assignable to parameter of type...

+ color('hsl(180, 50%, 50%)')
- color('hsl(180, 50%)')
// Argument of type '"hsl(180, 50%)"' is not assignable to parameter of type...

This is the moat. No other libraryβ€”not colord, chroma-js, or tinycolor2β€”can do this.

Installation

pnpm add use-color
npm install use-color
yarn add use-color

Usage

Parse Colors

import { color } from "use-color";

// All formats supported
const red = color("#ff0000");
const green = color("rgb(0, 255, 0)");
const blue = color("hsl(240, 100%, 50%)");
const purple = color("oklch(0.5 0.2 300)");
const coral = color("coral"); // Named CSS colors

// Object input
const custom = color({ r: 255, g: 128, b: 0 });

Transform Colors

const c = color("#3b82f6");

// Lightness
c.lighten(0.2); // Perceptually uniform lightening
c.darken(0.1);

// Saturation
c.saturate(0.3);
c.desaturate(0.2);
c.grayscale();

// Hue
c.rotate(45); // Rotate hue by degrees
c.complement(); // Rotate 180Β°

// Alpha
c.alpha(0.5); // Set alpha
c.opacify(0.1); // Increase alpha
c.transparentize(0.2); // Decrease alpha

// Others
c.invert();
c.mix(otherColor, 0.5);

Chain Transformations

const result = color("#e11d48")
  .lighten(0.1)
  .saturate(0.2)
  .rotate(15)
  .alpha(0.9)
  .toHex();

// "#ff2d5c"

Output Formats

const c = color("#3b82f6");

// Hex
c.toHex(); // "#3b82f6"
c.toHex8(); // "#3b82f6ff"
c.toHexShort(); // null (not compressible) or "#38f"

// RGB
c.toRgb(); // { r: 59, g: 130, b: 246, a: 1 }
c.toRgbString(); // "rgb(59, 130, 246)"
c.toRgbaString(); // "rgba(59, 130, 246, 1)"
c.toRgbModern(); // "rgb(59 130 246)"  (CSS Level 4)

// HSL
c.toHsl(); // { h: 217, s: 0.91, l: 0.60, a: 1 }
c.toHslString(); // "hsl(217, 91%, 60%)"
c.toHslaString(); // "hsla(217, 91%, 60%, 1)"
c.toHslModern(); // "hsl(217 91% 60%)"

// OKLCH (perceptually uniform)
c.toOklch(); // { l: 0.62, c: 0.19, h: 255, a: 1 }
c.toOklchString(); // "oklch(0.62 0.19 255)"

// Display P3 (wide gamut)
c.toP3String(); // "color(display-p3 0.35 0.52 0.93)"

// CSS (smart default)
c.toCss(); // "#3b82f6"
c.toCss({ format: "oklch" }); // "oklch(0.62 0.19 255)"

Safe Parsing

import { tryColor } from "use-color";

const result = tryColor(userInput);

if (result.ok) {
  // result.value is a Color instance
  console.log(result.value.toHex());
} else {
  // result.error is a ColorParseError
  console.error(result.error.message);
  console.error(result.error.code); // 'INVALID_HEX' | 'INVALID_RGB' | ...
}

Color Properties

const c = color("#3b82f6");

c.getAlpha(); // 1
c.getLightness(); // 0.62 (OKLCH lightness)
c.getChroma(); // 0.19 (OKLCH chroma)
c.getHue(); // 255 (OKLCH hue)

c.isDark(); // false
c.isLight(); // true

Accessibility

Built-in WCAG 2.1 contrast checking and auto-adjustment.

import { contrast, isReadable, ensureContrast, luminance } from "use-color";

const text = "#374151";
const background = "#ffffff";

// Relative luminance (WCAG formula)
luminance(text); // 0.18
luminance(background); // 1.0

// Contrast ratio (1-21)
contrast(text, background); // 7.49

// Readability checks
isReadable(text, background); // true (default: AA 4.5:1)
isReadable(text, background, { level: "AAA" }); // true (7:1)
isReadable(text, background, { level: "AA", size: "large" }); // true (3:1)

// Auto-adjust for accessibility
const adjusted = ensureContrast("#888888", background, 4.5);
adjusted.toHex(); // "#767676" (meets 4.5:1 ratio)

APCA (Experimental)

APCA is the next-generation contrast algorithm for WCAG 3.0.

import { apcaContrast } from "use-color";

// Returns Lc value (-108 to +106)
apcaContrast("#000000", "#ffffff"); // 106 (maximum contrast)
apcaContrast("#767676", "#ffffff"); // 63 (good for body text)

Note: APCA is still in development and not yet a W3C standard. Use for experimental projects.

Why OKLCH?

use-color uses OKLCH internally for all color manipulations. This ensures perceptually uniform results:

Operation HSL (traditional) OKLCH (use-color)
Lighten yellow vs blue Yellow appears brighter Both appear equally lighter
Desaturate Colors shift unexpectedly Consistent desaturation
Mix colors Muddy intermediates Vibrant, natural gradients

OKLCH is the color space used by Tailwind CSS v4, shadcn/ui, and modern design systems.

Display P3 Wide Gamut

93% of browsers support Display P3. All Apple devices since 2016 have P3 displays.

import { color, isInP3Gamut, clampToP3Gamut } from "use-color";

const vibrant = color("oklch(0.7 0.35 150)");

// Check gamut
isInP3Gamut(vibrant); // true (P3 is ~25% larger than sRGB)

// Output for modern displays
vibrant.toP3String(); // "color(display-p3 0.12 0.87 0.45)"

// Fallback for older displays
vibrant.toHex(); // "#00d96f" (clamped to sRGB)

Tree-Shakeable

Import only what you need. Every function is individually exportable.

// Only bundles these specific functions
import { parseHex, toHex, lighten } from "use-color";

const rgba = parseHex("#ff0000");
const lighter = lighten(rgba, 0.2);
const hex = toHex(lighter);

Modular Imports

For smaller bundles, import only what you need:

// Core only (~8KB gzip) - parsing, formatting, manipulation
import { color, lighten, darken, toHex } from "use-color/core";

// Accessibility functions (~3KB gzip)
import { contrast, isReadable, apcaContrast } from "use-color/a11y";

// Named colors (~2KB gzip) - adds 'coral', 'rebeccapurple', etc.
import { parseNamed, NAMED_COLORS } from "use-color/names";

// Display P3 wide gamut (~3KB gzip)
import { toP3String, isInP3Gamut } from "use-color/p3";
Import Size (gzip) Description
use-color ~10KB Full bundle with everything
use-color/core ~8KB Core color operations
use-color/a11y ~3KB WCAG contrast, APCA
use-color/names ~2KB CSS named colors
use-color/p3 ~3KB Display P3 gamut

Type Guards & Assertions

import { isHex, isRgb, isColor, assertHex } from "use-color";

// Type guards (return boolean)
isHex("#ff0000"); // true
isHex("#gggggg"); // false
isRgb("rgb(1,2,3)"); // true
isColor(something); // type narrowing

// Assertions (throw on invalid)
assertHex(userInput); // throws ColorParseError if invalid
// After this line, TypeScript knows userInput is valid

Comparison

Feature use-color colord chroma-js tinycolor2
Compile-time validation βœ… ❌ ❌ ❌
OKLCH native βœ… Plugin ❌ ❌
P3 wide gamut βœ… ❌ ❌ ❌
Accessibility Built-in Plugin ❌ Basic
Tree-shakeable βœ… βœ… ❌ ❌
Modular imports βœ… ❌ ❌ ❌
Bundle size (gzip) 8-10KB 2.1KB 16.7KB 5.3KB
TypeScript Excellent Good Poor Poor
Maintained βœ… ❌ 3yr ⚠️ ⚠️

Migration

From colord
- import { colord } from 'colord';
+ import { color } from 'use-color';

- const c = colord('#ff0000');
+ const c = color('#ff0000');

// Methods are mostly identical
c.lighten(0.1).toHex();

Key differences:

  • use-color uses OKLCH for transformations (better visual results)
  • Compile-time validation catches errors earlier
From chroma-js
- import chroma from 'chroma-js';
+ import { color, mix } from 'use-color';

- const c = chroma('#ff0000');
+ const c = color('#ff0000');

- chroma.mix('#ff0000', '#0000ff', 0.5);
+ mix(color('#ff0000'), color('#0000ff'), 0.5);
From tinycolor2
- import tinycolor from 'tinycolor2';
+ import { color } from 'use-color';

- const c = tinycolor('#ff0000');
+ const c = color('#ff0000');

- c.lighten(10).toString();
+ c.lighten(0.1).toHex();  // Note: use-color uses 0-1 range

API Reference

Factory Functions

Function Description
color(input) Create Color instance (throws on invalid)
tryColor(input) Safe parsing, returns Result<Color, ColorParseError>

Color Methods

Manipulation (returns new Color)

Method Description
.lighten(amount) Increase lightness (0-1)
.darken(amount) Decrease lightness (0-1)
.saturate(amount) Increase chroma (0-1)
.desaturate(amount) Decrease chroma (0-1)
.grayscale() Remove all chroma
.rotate(degrees) Rotate hue
.complement() Rotate hue 180Β°
.alpha(value) Set alpha (0-1)
.opacify(amount) Increase alpha
.transparentize(amount) Decrease alpha
.invert() RGB inversion
.invertLightness() OKLCH lightness inversion
.mix(color, ratio) Mix with another color

Output (returns string/object)

Method Description
.toHex() "#rrggbb"
.toHex8() "#rrggbbaa"
.toRgb() { r, g, b, a }
.toRgbString() "rgb(r, g, b)"
.toHsl() { h, s, l, a }
.toHslString() "hsl(h, s%, l%)"
.toOklch() { l, c, h, a }
.toOklchString() "oklch(l c h)"
.toP3String() "color(display-p3 r g b)"
.toCss(options?) Smart CSS output

Properties

Method Description
.getAlpha() Get alpha value
.getLightness() Get OKLCH lightness
.getChroma() Get OKLCH chroma
.getHue() Get OKLCH hue
.isDark() Lightness < 0.5
.isLight() Lightness >= 0.5

Standalone Functions

All Color methods are also available as standalone, tree-shakeable functions:

import { lighten, darken, mix, contrast, toHex } from "use-color";

License

MIT Β© Junho Yeo

About

πŸ›Ό A TypeScript-first, OKLCH-based, framework-agnostic color library that validates color strings at compile time

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 6

Languages