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.
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.
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
- You are starting something new in 2026. The last release was 1.6.0 in February 2023 and the last commit to the repository was June 2024, with 77 issues open (104 counting PRs). It is not abandoned in the sense of being broken, but nobody is fixing anything either
- You need modern color spaces. There is no LAB, LCH, OKLCH, OKLAB, Display P3, or color-mix support at all, so it cannot express or interpolate the colors that current CSS and design tools produce. Mixing in sRGB also produces the muddy midpoints that perceptual spaces exist to avoid
- You want TypeScript types in the box. None are shipped; you install @types/tinycolor2 from DefinitelyTyped separately and hope it matches the runtime
- You expect the objects to be immutable. tinycolor('#f00').lighten(20) mutates and returns the same instance, so a shared color constant passed through a helper is permanently changed. You have to remember .clone() and nothing warns you
- You want invalid input to fail loudly. An unparseable string produces an object where isValid() is false but every conversion method quietly behaves like black, so a typo in a config file ships a black button instead of an error
- Your accessibility work needs current guidance. getBrightness uses the WCAG 1.0 formula from a 2000-era working draft, and readability uses the WCAG 2.0 contrast ratio, which does not account for the perceptual issues APCA was designed to fix
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() // 0Alpha 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
| Package | Registry | Pick it when |
|---|---|---|
| @ctrl/tinycolor | npm | You want the same API with TypeScript types built in and an actively maintained fork, which makes it the lowest-effort migration off this package |
| colord | npm | You want a small immutable library with a plugin system for LCH, a11y, mixing, and named colors so you only pay for what you use |
| culori | npm | You need modern color spaces, OKLCH interpolation, gamut mapping, and CSS Color 4 parsing rather than sRGB manipulation |
| chroma-js | npm | You are building data visualisation scales and want perceptually even gradients and classed color ramps |