Manipulação de Cores em JavaScript: Bibliotecas e Técnicas
Embed This Widget
Add the script tag and a data attribute to embed this widget.
Embed via iframe for maximum compatibility.
<iframe src="https://colorfyi.com/iframe/entity//" width="420" height="400" frameborder="0" style="border:0;border-radius:10px;max-width:100%" loading="lazy"></iframe>
Paste this URL in WordPress, Medium, or any oEmbed-compatible platform.
https://colorfyi.com/entity//
Add a dynamic SVG badge to your README or docs.
[](https://colorfyi.com/entity//)
Use the native HTML custom element.
A cor em aplicações web não é um valor estático que você cola de um arquivo de design e deixa de lado. As cores ficam mais claras ao passar o mouse, mais escuras quando pressionadas, se ajustam aos requisitos de contraste de acessibilidade, animam entre estados e se adaptam a temas selecionados pelo usuário. Tudo isso exige manipulação programática de cores — a capacidade de analisar, transformar e gerar valores de cor em JavaScript em tempo de execução.
Este guia cobre como o JavaScript lida com cores nativamente, a matemática por trás das transformações comuns, uma comparação das três bibliotecas mais usadas e como construir um utilitário de cores mínimo quando você quer zero dependências.
Analisando Hex, RGB e HSL em JavaScript
O JavaScript não tem um tipo de cor nativo. As cores chegam como strings — "#FF5733", "rgb(255, 87, 51)", "hsl(11, 100%, 60%)" — e você deve analisá-las antes de fazer qualquer operação matemática.
Analisando Códigos Hex
Uma cor hex é uma codificação compacta de três (ou quatro, com alfa) inteiros de um byte em base 16. Analisá-la é uma questão de fatiar a string e chamar parseInt:
function parseHex(hex) {
// Normalizar: remover # e expandir abreviação (#F53 → #FF5533)
const clean = hex.replace('#', '');
const full = clean.length === 3
? clean.split('').map(c => c + c).join('')
: clean;
return {
r: parseInt(full.slice(0, 2), 16),
g: parseInt(full.slice(2, 4), 16),
b: parseInt(full.slice(4, 6), 16),
};
}
parseHex('#FF5733'); // { r: 255, g: 87, b: 51 }
parseHex('#F53'); // { r: 255, g: 85, b: 51 }
O inverso — inteiros de volta a uma string hex — usa toString(16) com preenchimento de zero:
function toHex({ r, g, b }) {
return '#' + [r, g, b]
.map(v => Math.round(v).toString(16).padStart(2, '0'))
.join('')
.toUpperCase();
}
toHex({ r: 255, g: 87, b: 51 }); // '#FF5733'
Analisando Strings RGB
Strings RGB do DOM ou do CSS-in-JS frequentemente chegam como "rgb(255, 87, 51)". Uma regex extrai os três valores:
function parseRgb(str) {
const match = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (!match) throw new Error(`RGB inválido: ${str}`);
return {
r: parseInt(match[1]),
g: parseInt(match[2]),
b: parseInt(match[3]),
};
}
parseRgb('rgb(255, 87, 51)'); // { r: 255, g: 87, b: 51 }
parseRgb('rgba(255, 87, 51, 0.5)'); // { r: 255, g: 87, b: 51 }
Analisando Strings HSL
Strings HSL — "hsl(11, 100%, 60%)" — exigem a extração de valores de graus e porcentagem:
function parseHsl(str) {
const match = str.match(/hsla?\((\d+),\s*([\d.]+)%,\s*([\d.]+)%/);
if (!match) throw new Error(`HSL inválido: ${str}`);
return {
h: parseFloat(match[1]),
s: parseFloat(match[2]),
l: parseFloat(match[3]),
};
}
Convertendo Entre RGB e HSL
A maioria da matemática de cores opera em RGB (para mistura) ou HSL/HSV (para ajustes intuitivos). Converter entre eles é a primeira habilidade que você precisa:
function rgbToHsl({ r, g, b }) {
const rn = r / 255, gn = g / 255, bn = b / 255;
const max = Math.max(rn, gn, bn);
const min = Math.min(rn, gn, bn);
const l = (max + min) / 2;
const d = max - min;
if (d === 0) return { h: 0, s: 0, l: Math.round(l * 100) };
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
let h;
switch (max) {
case rn: h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6; break;
case gn: h = ((bn - rn) / d + 2) / 6; break;
default: h = ((rn - gn) / d + 4) / 6;
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100),
};
}
rgbToHsl({ r: 255, g: 87, b: 51 }); // { h: 11, s: 100, l: 60 }
Matemática de Cores: Clarear, Escurecer, Dessaturar
Com uma cor em HSL, ajustá-la é aritmética. Os três eixos do HSL mapeiam diretamente para os ajustes que os designers mais frequentemente solicitam.
Clareando e Escurecendo
Aumentar ou diminuir o canal l (luminosidade) é a abordagem direta:
function lighten(hex, amount) {
const hsl = rgbToHsl(parseHex(hex));
hsl.l = Math.min(100, hsl.l + amount);
return hslToHex(hsl);
}
function darken(hex, amount) {
const hsl = rgbToHsl(parseHex(hex));
hsl.l = Math.max(0, hsl.l - amount);
return hslToHex(hsl);
}
lighten('#FF5733', 15); // Coral mais claro
darken('#FF5733', 15); // Vermelho-laranja mais escuro
Isso funciona bem para pequenos ajustes. Para gerar uma escala completa de 50 a 950 (como o Tailwind CSS faz), a matemática é mais envolvida porque a luminosidade percebida não é linear em HSL — o Gerador de Sombras lida com isso com distribuição ponderada perceptualmente.
Dessaturação
Diminuir o canal s (saturação) em direção a 0 transforma qualquer cor em cinza:
function desaturate(hex, amount) {
const hsl = rgbToHsl(parseHex(hex));
hsl.s = Math.max(0, hsl.s - amount);
return hslToHex(hsl);
}
function grayscale(hex) {
return desaturate(hex, 100);
}
desaturate('#FF5733', 50); // Laranja discreto e de baixa saturação
grayscale('#FF5733'); // Cinza puro com a mesma luminosidade
Misturando Duas Cores
A interpolação linear no espaço RGB é a mistura mais simples:
function mix(hex1, hex2, weight = 0.5) {
const c1 = parseHex(hex1);
const c2 = parseHex(hex2);
return toHex({
r: Math.round(c1.r * weight + c2.r * (1 - weight)),
g: Math.round(c1.g * weight + c2.g * (1 - weight)),
b: Math.round(c1.b * weight + c2.b * (1 - weight)),
});
}
mix('#FF5733', '#FFFFFF', 0.7); // 70% coral, 30% branco → um tint
mix('#FF5733', '#000000', 0.7); // 70% coral, 30% preto → um shade
A mistura em RGB produz resultados perceptualmente planos entre cores complementares. Para melhores pontos intermediários, bibliotecas como chroma.js oferecem mistura nos espaços OKLCH ou Lab.
Taxa de Contraste (WCAG)
A taxa de contraste WCAG é calculada a partir da luminância relativa, não dos valores brutos dos canais. A luminância requer correção de gama — o inverso da codificação aplicada quando os valores sRGB são armazenados:
function relativeLuminance({ r, g, b }) {
const linearize = (v) => {
const sRGB = v / 255;
return sRGB <= 0.04045
? sRGB / 12.92
: Math.pow((sRGB + 0.055) / 1.055, 2.4);
};
const R = linearize(r), G = linearize(g), B = linearize(b);
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
}
function contrastRatio(hex1, hex2) {
const L1 = relativeLuminance(parseHex(hex1));
const L2 = relativeLuminance(parseHex(hex2));
const lighter = Math.max(L1, L2);
const darker = Math.min(L1, L2);
return (lighter + 0.05) / (darker + 0.05);
}
contrastRatio('#FF5733', '#FFFFFF'); // ~3,0 — reprova WCAG AA para texto normal
contrastRatio('#000000', '#FFFFFF'); // 21,0 — contraste máximo
O WCAG AA exige 4,5:1 para texto normal e 3:1 para texto grande. Verifique qualquer par com o Conversor de Cores.
Comparação das Bibliotecas Populares: chroma.js, culori, tinycolor2
Para qualquer coisa além de ajustes simples, uma biblioteca dedicada economiza tempo significativo e evita bugs de casos extremos na matemática de cores.
chroma.js
Tamanho: ~13KB gzipado | Maturidade: 2013, ativamente mantido | Licença: BSD
chroma.js é a biblioteca de cores JavaScript mais conhecida. Sua API é fluente e encadeável:
import chroma from 'chroma-js';
// Analisar qualquer formato
const color = chroma('#FF5733');
// Ajustar
color.darken(1).hex(); // '#D93B10'
color.lighten(1).hex(); // '#FF8066'
color.saturate(0.5).hex(); // '#FF4719'
color.desaturate(0.5).hex();// '#F2653F'
// Converter
color.rgb(); // [255, 87, 51]
color.hsl(); // [11, 1, 0.6]
color.lab(); // [52.8, 47.1, 44.7]
color.oklch();// [0.63, 0.19, 27.5]
// Misturar em diferentes espaços de cor
chroma.mix('#FF5733', '#3B82F6', 0.5, 'rgb'); // Em RGB (ponto médio plano)
chroma.mix('#FF5733', '#3B82F6', 0.5, 'oklch'); // Em OKLCH (ponto médio vibrante)
// Geração de escala
const scale = chroma.scale(['#FFF5F5', '#FF5733', '#7F0000'])
.mode('oklch')
.colors(9);
// Contraste
chroma.contrast('#FF5733', '#FFFFFF'); // 3.0
chroma.js é a escolha certa quando você precisa de uma biblioteca abrangente e bem documentada com suporte mínimo para configuração e boa suporte para mistura em espaços de cor.
culori
Tamanho: ~6KB gzipado (tree-shakeable) | Maturidade: 2019, ativamente mantido | Licença: MIT
culori é uma biblioteca moderna e funcional projetada para tree-shaking. Cada operação é uma função autônoma — você importa apenas o que usa:
import { parse, formatHex, oklch, interpolate, formatCss } from 'culori';
// Analisar
const color = parse('#FF5733'); // { mode: 'rgb', r: 1, g: 0.34, b: 0.2 }
// Converter para OKLCH
const inOklch = oklch(color); // { mode: 'oklch', l: 0.63, c: 0.19, h: 27.5 }
// Ajustar luminosidade
const lighter = { ...inOklch, l: inOklch.l + 0.1 };
formatHex(lighter); // '#FF8469'
// Interpolação para gradientes/animação
const gradient = interpolate(['#FF5733', '#3B82F6'], 'oklch');
formatCss(gradient(0.5)); // Cor do ponto médio em formato CSS
// Geração de escala
import { samples } from 'culori';
const stops = samples(5).map(t => formatHex(gradient(t)));
// ['#FF5733', '#E85E56', '#9B6DE3', '#6186F0', '#3B82F6']
culori opera em objetos simples com uma propriedade mode, o que facilita a serialização de cores, seu armazenamento em estado ou envio por uma rede. É a melhor escolha para projetos TypeScript modernos onde o tamanho do bundle importa e o tree-shaking está em uso.
tinycolor2
Tamanho: ~5KB gzipado | Maturidade: 2012, estável (menos ativo) | Licença: MIT
tinycolor2 é o menor e mais permissivo analisador — ele aceita quase qualquer formato de string de cor, incluindo nomes de cores CSS, e funciona sem configuração:
import tinycolor from 'tinycolor2';
// Analisar quase qualquer coisa
tinycolor('red').toHexString(); // '#FF0000'
tinycolor('hsl(11, 100%, 60%)').toHexString(); // '#FF5733'
tinycolor('#F53').toHexString(); // '#FF5533'
// Ajustar
tinycolor('#FF5733').lighten(15).toHexString(); // '#FF8966'
tinycolor('#FF5733').darken(15).toHexString(); // '#C92B04'
tinycolor('#FF5733').desaturate(30).toHexString(); // '#EB6643'
tinycolor('#FF5733').spin(180).toHexString(); // '#33AEff' (complementar)
// Legibilidade / contraste
tinycolor.readability('#FF5733', '#FFFFFF'); // 3.0
tinycolor.isReadable('#FF5733', '#FFFFFF'); // false (reprova WCAG AA)
tinycolor.isReadable('#FF5733', '#FFFFFF', { level: 'AA', size: 'large' }); // true
// Harmonias de cores
tinycolor('#FF5733').triad(); // [TinyColor, TinyColor, TinyColor]
tinycolor('#FF5733').analogous(); // 6 cores análogas
tinycolor('#FF5733').complement(); // Única cor complementar
tinycolor2 é a escolha certa para projetos que precisam de análise confiável de strings de cor inseridas pelo usuário (que podem estar em qualquer formato) e manipulações básicas sem trazer uma dependência maior.
Resumo da Comparação de Bibliotecas
| Recurso | chroma.js | culori | tinycolor2 |
|---|---|---|---|
| Tamanho do bundle | ~13KB | ~6KB (tree-shakeable) | ~5KB |
| Espaços de cor | RGB, HSL, Lab, LCH, OKLCH | 20+ incluindo OKLCH, P3 | RGB, HSL, HSV |
| Estilo da API | Fluente/encadeável | Funcional | Orientado a objetos |
| TypeScript | Tipos da comunidade | Integrado | Tipos da comunidade |
| Mistura de cores | RGB, HSL, Lab, OKLCH | Qualquer espaço de cor | Somente RGB |
| Melhor para | Uso abrangente, visualização de dados | Bundles TS modernos | Análise, operações simples |
Construindo um Utilitário de Cores do Zero
Para uma aplicação de produção que só precisa de análise hex, clareamento, escurecimento e verificação de contraste, um utilitário sem dependências é frequentemente a melhor escolha arquitetural. Aqui está uma implementação completa:
// color-utils.js
export function parseHex(hex) {
const clean = hex.replace('#', '');
const full = clean.length === 3
? clean.split('').map(c => c + c).join('')
: clean;
return {
r: parseInt(full.slice(0, 2), 16),
g: parseInt(full.slice(2, 4), 16),
b: parseInt(full.slice(4, 6), 16),
};
}
export function toHex({ r, g, b }) {
return '#' + [r, g, b]
.map(v => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0'))
.join('').toUpperCase();
}
function rgbToHsl({ r, g, b }) {
const rn = r / 255, gn = g / 255, bn = b / 255;
const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn);
const l = (max + min) / 2;
const d = max - min;
if (d === 0) return { h: 0, s: 0, l };
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
let h;
switch (max) {
case rn: h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6; break;
case gn: h = ((bn - rn) / d + 2) / 6; break;
default: h = ((rn - gn) / d + 4) / 6;
}
return { h, s, l };
}
function hslToRgb({ h, s, l }) {
if (s === 0) {
const v = Math.round(l * 255);
return { r: v, g: v, b: v };
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
return {
r: Math.round(hue2rgb(p, q, h + 1/3) * 255),
g: Math.round(hue2rgb(p, q, h) * 255),
b: Math.round(hue2rgb(p, q, h - 1/3) * 255),
};
}
export function lighten(hex, amount) {
const hsl = rgbToHsl(parseHex(hex));
return toHex(hslToRgb({ ...hsl, l: Math.min(1, hsl.l + amount / 100) }));
}
export function darken(hex, amount) {
const hsl = rgbToHsl(parseHex(hex));
return toHex(hslToRgb({ ...hsl, l: Math.max(0, hsl.l - amount / 100) }));
}
export function desaturate(hex, amount) {
const hsl = rgbToHsl(parseHex(hex));
return toHex(hslToRgb({ ...hsl, s: Math.max(0, hsl.s - amount / 100) }));
}
function linearize(v) {
const sRGB = v / 255;
return sRGB <= 0.04045 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4);
}
export function relativeLuminance(hex) {
const { r, g, b } = parseHex(hex);
return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
}
export function contrastRatio(hex1, hex2) {
const L1 = relativeLuminance(hex1);
const L2 = relativeLuminance(hex2);
const lighter = Math.max(L1, L2);
const darker = Math.min(L1, L2);
return (lighter + 0.05) / (darker + 0.05);
}
export function isWcagAA(foreground, background, largeText = false) {
const ratio = contrastRatio(foreground, background);
return ratio >= (largeText ? 3.0 : 4.5);
}
export function bestTextColor(background) {
const L = relativeLuminance(background);
return L > 0.179 ? '#000000' : '#FFFFFF';
}
Uso:
import { lighten, darken, contrastRatio, bestTextColor } from './color-utils.js';
const brand = '#FF5733';
const hover = darken(brand, 10); // Mais escuro para :hover
const light = lighten(brand, 30); // Tint claro para fundos
const text = bestTextColor(brand); // Preto ou branco para texto sobre a cor da marca
console.log(contrastRatio(text, brand)); // Deve ser ≥ 4.5
Considerações de Desempenho para Cores em Tempo de Execução
Cache Agressivo
Os cálculos de cores são referencialmente transparentes — a mesma entrada hex sempre produz a mesma saída. Um simples wrapper de memoização evita recálculos a cada renderização:
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (!cache.has(key)) cache.set(key, fn(...args));
return cache.get(key);
};
}
const lightenCached = memoize(lighten);
const contrastCached = memoize(contrastRatio);
Propriedades Customizadas CSS em vez de JavaScript
Para tematização dinâmica, não recalcule cores em JavaScript a cada mudança de estado. Calcule a paleta uma vez, escreva-a nas propriedades customizadas CSS e deixe o CSS lidar com cada componente:
function applyTheme(brandHex) {
const root = document.documentElement;
root.style.setProperty('--brand', brandHex);
root.style.setProperty('--brand-dark', darken(brandHex, 10));
root.style.setProperty('--brand-light', lighten(brandHex, 30));
root.style.setProperty('--on-brand', bestTextColor(brandHex));
}
// Chamado uma vez quando a cor da marca muda, não a cada renderização
applyTheme('#FF5733');
Evite Analisar Strings em Loops de Renderização
Analisar "rgb(255, 87, 51)" dentro de uma função de renderização React que roda 60 vezes por segundo é ineficiente. Analise uma vez, armazene o resultado e passe objetos de cor estruturados pela árvore de componentes em vez de strings brutas.
// Custoso: analisa a cada chamada
const color = chroma(colorString).darken(1).hex();
// Melhor: analise uma vez, transforme muitas vezes
const parsed = chroma(colorString);
const darkVariant = parsed.darken(1).hex();
const lightVariant = parsed.lighten(1).hex();
Para geração de cores em tempo de execução em escala — seletores de cor que atualizam a cada movimento do mouse, ou visualizações com centenas de cores calculadas — culori é a melhor escolha porque sua arquitetura funcional e sem estado e o tamanho reduzido do bundle têm o menor overhead por chamada.
Principais Conclusões
- JavaScript não tem tipo de cor nativo — analise strings hex, RGB e HSL em objetos estruturados antes de fazer qualquer cálculo.
- A aritmética HSL (ajustar
lpara luminosidade,spara saturação) cobre a maioria das transformações de design comuns diretamente. - A taxa de contraste WCAG requer cálculo de luminância com correção de gama — não a aproxime com a matemática do canal de luminosidade.
- chroma.js é a biblioteca mais completa com excelente suporte a espaços de cor incluindo mistura OKLCH; culori é a escolha moderna tree-shakeable para projetos TypeScript; tinycolor2 é o analisador mais permissivo para cenários de entrada do usuário.
- Um utilitário sem dependências é viável para aplicações que precisam apenas de análise, clareamento/escurecimento e contraste — a implementação completa tem menos de 100 linhas.
- Faça cache agressivo das cores calculadas e escreva os resultados nas propriedades customizadas CSS em vez de recalcular por renderização.
- Use o Conversor de Cores para verificar visualmente qualquer transformação de cor, e o Gerador de Sombras para gerar escalas completas de 50 a 950 compatíveis com Tailwind a partir de um único hex de marca.