mrkeyoor.com_
Thu 06 Aug 10:54 UTC
npmUtilsupdated 06 Aug 2026

tinycolor2

tinycolor2 is a single-file, dependency-free library for parsing and manipulating colors in JavaScript. You pass it almost anything that looks like a color, a hex string with or without the hash, an rgb or hsl string with or without commas, an object such as { r: 255, g: 0, b: 0 }, or one of 149 CSS color names, and you get back an object that can convert to any other format and adjust the color. Conversions cover hex, 8-digit hex with alpha, rgb, percentage rgb, hsl, hsv, and CSS names. Adjustments cover lighten, darken, brighten, saturate, desaturate, greyscale, and spin for rotating the hue. It also generates palettes (analogous, monochromatic, triad, tetrad, split complement) and computes WCAG 2.0 contrast ratios so you can check whether text will be readable on a background.

Verdict

It still parses everything you throw at it and it is tiny, which is why 13.5 million weekly downloads keep flowing to a package that has not shipped a release since early 2023. For new work, @ctrl/tinycolor gives you the same API with types and a pulse, and colord or culori give you the color spaces that modern CSS actually uses.

API stability5/5The surface has not changed meaningfully since the 1.x line began, and code written against it years ago runs unchanged today; stability here is partly a consequence of the project having stopped rather than a deliberate policy.
Docs4/5The README documents every method with a runnable example and states awkward behaviour plainly, including that an invalid color acts like black and that modification methods return the instance for chaining; there is no separate docs site and nothing explains the unit asymmetry between input and output.
Maintenance2/5Version 1.6.0 dates from February 2023 and the last push to the repository was June 2024, with 77 open issues (104 counting PRs) and no roadmap; nothing is broken today because the scope is fixed, but a bug found tomorrow will not be fixed.
Ecosystem4/5Around 13.6M weekly downloads and a long tail of design tools, color pickers, and UI kits depending on it, plus community types on DefinitelyTyped and a maintained fork at @ctrl/tinycolor; almost all of that is legacy dependency rather than new adoption.

Use it if

  • You need to accept color input from users or from a database where the format is inconsistent, and you want one function that swallows '#f00', 'rgb 255 0 0', 'red', and { h: 0, s: 1, l: 0.5 } without you writing the parser
  • You want a contrast check without pulling an accessibility framework: tinycolor.readability(a, b) gives the WCAG 2.0 ratio and isReadable applies the AA and AAA thresholds
  • You are generating a theme or a chart palette at runtime and need lighten, darken, and spin to derive variants from one brand color
  • You need this to work in a browser script tag, in CommonJS, and in ESM without a build step; the package ships UMD, CJS, and ESM builds of the same file and has zero dependencies
  • You are maintaining an existing project that already depends on it, where the API is doing its job and a migration would touch every theme file
Skip it if

Setup reality

npm install tinycolor2 and import it; there is genuinely no configuration, no peer dependency, and no native build. Two things are worth knowing before you commit. First, types: nothing is bundled, so a TypeScript project also needs npm install -D @types/tinycolor2, and because the runtime has not changed since 2023 while the type package is maintained separately, mismatches show up as types that are stricter or looser than reality. Second, the units are asymmetric in a way that causes real bugs. On input, saturation and lightness accept 0 to 1 or 0% to 100%, and hue accepts 0 to 360 or a percentage. On output, toHsl and toHsv return hue in degrees but saturation, lightness, and value as fractions between 0 and 1, so feeding a toHsl result into code that expects percentages silently gives you a nearly grey color. tinycolor.fromRatio exists specifically to read rgb and hue values in the 0 to 1 range instead. Alpha has its own rounding surprise: parsing an 8-digit hex such as #ff000080 gives getAlpha() of 0.5019607843137255 rather than 0.5, because the byte is divided by 255, which breaks strict equality checks in tests. The package tarball also contains the test suites for the CJS and ESM builds plus a vendored assertion file, so the install footprint is larger than the roughly 5.4 KB the minified bundle gzips to; only the bundle reaches your users.

Patterns

Accept color input in whatever format it arrivesparse-any-input

import tinycolor from 'tinycolor2'

tinycolor('#f0f0f6')
tinycolor('f0f0f688')            // 8-digit hex, hash optional
tinycolor('rgb 255 0 0')         // commas and parens optional
tinycolor('hsl(0, 100%, 50%)')
tinycolor('blanchedalmond')      // 149 CSS names, case insensitive
tinycolor({ r: 255, g: 0, b: 0, a: 0.5 })
tinycolor.fromRatio({ r: 1, g: 0, b: 0 })

fromRatio is the one you want when your values are already 0 to 1, because the normal constructor reads r, g, b as 0 to 255. The parser is deliberately permissive, which is convenient for user input and unhelpful when you wanted validation.

Check that a string was actually a colorvalidate-input

const c = tinycolor(userInput)

if (!c.isValid()) {
  throw new Error(`not a color: ${userInput}`)
}

return c.toHexString()

// without the guard:
tinycolor('nope').toHexString()  // '#000000'

This check is not optional. An invalid color does not throw and does not return null; it behaves as black through every method, so a bad value in a theme config becomes a black background that nobody notices until a screenshot.

Convert between color formatsconvert-formats

const c = tinycolor('red')

c.toHexString()            // '#ff0000'
c.toHex8String()           // '#ff0000ff'
c.toRgbString()            // 'rgb(255, 0, 0)'
c.toHslString()            // 'hsl(0, 100%, 50%)'
c.toName()                 // 'red'
c.toString('hsv')          // 'hsv(0, 100%, 100%)'

c.toHsl()                  // { h: 0, s: 1, l: 0.5, a: 1 }

The string forms use percentages, the object forms do not: toHsl gives hue in degrees but saturation and lightness as fractions of 1. Passing that object straight into something expecting 0 to 100 gives you a nearly grey color with no error.

Stop the modification methods from wrecking your source coloravoid-mutation

const brand = tinycolor('#3366cc')

// wrong: brand itself is now lighter
const hover = brand.lighten(10).toHexString()

// right
const hover2 = brand.clone().lighten(10).toHexString()
const active = brand.clone().darken(10).toHexString()

lighten, darken, brighten, saturate, desaturate, greyscale, and spin all mutate the instance and return it, so chaining works but the original is gone. This is the single most common bug with this library; clone() before every derivation.

Build a shade scale from one brand colorderive-theme-shades

function scale(hex) {
  const base = tinycolor(hex)
  const steps = [40, 30, 20, 10, 0, 10, 20, 30, 40]
  return steps.map((amount, i) =>
    (i < 4 ? base.clone().lighten(amount)
           : i === 4 ? base.clone()
           : base.clone().darken(amount)
    ).toHexString()
  )
}

lighten and darken operate on HSL lightness, so they flatten out near white and black: lighten(100) is always #ffffff and darken(100) is always #000000. brighten() adjusts RGB channels instead and gives a different, often better looking curve for mid-tones.

Check text contrast against a backgroundcontrast-check

tinycolor.readability('#000', '#fff')   // 21
tinycolor.readability('#777', '#fff')  // ~4.47

tinycolor.isReadable('#fff', '#f00', { level: 'AA', size: 'small' })   // false
tinycolor.isReadable('#fff', '#f00', { level: 'AA', size: 'large' })  // true

tinycolor.mostReadable('#000', ['#111', '#488', '#c0ffee']).toHexString()

This is the WCAG 2.0 contrast ratio: 4.5 for AA small text, 3 for AA large, 7 for AAA small. mostReadable picks the best candidate but can still return something that fails, so pass { includeFallbackColors: true } or check the result with isReadable.

Pick readable text for an arbitrary backgroundlight-or-dark-text

function textOn(bg) {
  const c = tinycolor(bg)
  return c.isLight() ? '#111111' : '#ffffff'
}

// or let it choose and verify
const fg = tinycolor.mostReadable(bg, ['#111111', '#ffffff'], {
  includeFallbackColors: true,
})

isLight and isDark use getBrightness, which is the WCAG 1.0 perceived brightness formula from a 2000-era draft, not the WCAG 2.0 luminance used by readability(). The two can disagree on mid-tone greens and yellows, so prefer mostReadable when it matters.

Read and set transparencyalpha-handling

const c = tinycolor('rgba(255, 0, 0, .5)')
c.getAlpha()               // 0.5
c.setAlpha(0.25)
c.toRgbString()            // 'rgba(255, 0, 0, 0.25)'

tinycolor('#ff000080').getAlpha()   // 0.5019607843137255
tinycolor('transparent').getAlpha() // 0

Alpha parsed from 8-digit hex is a byte divided by 255, so it is almost never a round number. Comparing it to 0.5 in a test fails; round it before asserting. setAlpha mutates like the other modification methods.

Generate a related palettegenerate-palette

const base = tinycolor('#f00')

base.analogous().map(c => c.toHexString())        // 6 colors
base.monochromatic(5).map(c => c.toHexString())
base.triad().map(c => c.toHexString())
base.tetrad().map(c => c.toHexString())
base.splitcomplement().map(c => c.toHexString())
base.complement().toHexString()

These return fresh TinyColor instances rather than mutating base, which is the opposite of the modification methods. They rotate hue in HSL, so the results are mathematically even but not perceptually even; for charts a perceptual library gives better spacing.

Blend two colors and test equalitymix-and-compare

tinycolor.mix('#f00', '#00f', 50).toHexString()   // '#800080'

tinycolor.equals('#f00', 'rgb(255, 0, 0)')        // true
tinycolor.equals('#f00', 'hsl(0, 100%, 50%)')     // true

tinycolor.random().toHexString()

mix interpolates in sRGB, which is why blending complementary colors passes through grey or brown rather than a colour a designer would pick. equals compares the resolved rgba values, so different notations for the same color match.

Use it from TypeScripttypescript-setup

npm install tinycolor2
npm install -D @types/tinycolor2

import tinycolor, { type Instance, type ColorInput } from 'tinycolor2'

function normalize(input: ColorInput): string {
  const c: Instance = tinycolor(input)
  return c.isValid() ? c.toHexString() : '#000000'
}

The runtime package ships no types at all. @types/tinycolor2 comes from DefinitelyTyped on its own release schedule, so it is the community's description of a library that stopped changing in 2023 rather than something the authors verify.

Use it without a bundlerbrowser-script-tag

<script src="tinycolor.js"></script>
<script>
  var color = tinycolor('red');
  document.body.style.background = color.lighten(30).toHexString();
</script>

<script type="module">
  import tinycolor from 'https://esm.sh/tinycolor2';
</script>

The package ships UMD, CommonJS, and ESM builds of the same source, which is why it still drops into a legacy page with no tooling. The minified bundle gzips to roughly 5.4 KB, small enough that the size is rarely the reason to replace it.

Alternatives

PackageRegistryPick it when
@ctrl/tinycolornpmYou want the same API with TypeScript types built in and an actively maintained fork, which makes it the lowest-effort migration off this package
colordnpmYou want a small immutable library with a plugin system for LCH, a11y, mixing, and named colors so you only pay for what you use
culorinpmYou need modern color spaces, OKLCH interpolation, gamut mapping, and CSS Color 4 parsing rather than sRGB manipulation
chroma-jsnpmYou are building data visualisation scales and want perceptually even gradients and classed color ramps