Tutoriels

Manipulation des couleurs en JavaScript : bibliothèques et techniques

13 min de lecture

La couleur dans les applications web n'est pas une valeur statique que l'on colle depuis un fichier de design et que l'on laisse telle quelle. Les couleurs s'éclaircissent au survol, s'assombrissent à l'appui, s'adaptent aux exigences de contraste d'accessibilité, s'animent entre les états et s'ajustent aux thèmes choisis par l'utilisateur. Tout cela requiert une manipulation programmatique des couleurs — la capacité d'analyser, de transformer et de générer des valeurs de couleur en JavaScript à l'exécution.

Ce guide couvre la gestion native des couleurs en JavaScript, les mathématiques derrière les transformations courantes, une comparaison des trois bibliothèques les plus utilisées, et la façon de construire un utilitaire de couleur minimal quand on souhaite zéro dépendance.


Analyser les codes hex, RGB et HSL en JavaScript

JavaScript ne possède pas de type de couleur natif. Les couleurs arrivent sous forme de chaînes — "#FF5733", "rgb(255, 87, 51)", "hsl(11, 100%, 60%)" — et vous devez les analyser vous-même avant d'effectuer tout calcul.

Analyser les codes hex

Un code hex est un encodage compact de trois (ou quatre, avec alpha) entiers d'un octet en base 16. L'analyser consiste à découper la chaîne et à appeler parseInt :

function parseHex(hex) {
  // Normalisation : retirer # et développer le raccourci (#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 }

L'opération inverse — des entiers vers une chaîne hex — utilise toString(16) avec un remplissage à zéro :

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'

Analyser les chaînes RGB

Les chaînes RGB provenant du DOM ou de CSS-in-JS arrivent souvent sous la forme "rgb(255, 87, 51)". Une expression régulière extrait les trois valeurs :

function parseRgb(str) {
  const match = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
  if (!match) throw new Error(`RGB invalide : ${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 }

Analyser les chaînes HSL

Les chaînes HSL — "hsl(11, 100%, 60%)" — nécessitent d'extraire les valeurs de degré et de pourcentage :

function parseHsl(str) {
  const match = str.match(/hsla?\((\d+),\s*([\d.]+)%,\s*([\d.]+)%/);
  if (!match) throw new Error(`HSL invalide : ${str}`);
  return {
    h: parseFloat(match[1]),
    s: parseFloat(match[2]),
    l: parseFloat(match[3]),
  };
}

Convertir entre RGB et HSL

La plupart des calculs de couleur opèrent en RGB (pour le mélange) ou en HSL/HSV (pour les ajustements intuitifs). La conversion entre les deux est la première compétence dont vous avez besoin :

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 }

Calculs de couleurs : éclaircir, assombrir, désaturer

Une fois qu'une couleur est en HSL, l'ajuster est une question d'arithmétique. Les trois axes de HSL correspondent directement aux ajustements les plus fréquemment demandés par les designers.

Éclaircir et assombrir

Augmenter ou diminuer le canal l (luminosité) est l'approche directe :

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); // Corail plus clair
darken('#FF5733', 15);  // Rouge-orange plus foncé

Cela fonctionne bien pour de petits ajustements. Pour générer une gamme complète de 50 à 950 (comme Tailwind CSS), les calculs sont plus complexes car la luminosité perçue n'est pas linéaire en HSL — le Générateur de nuances gère cela avec une distribution pondérée perceptuellement.

Désaturation

Diminuer le canal s (saturation) vers 0 transforme n'importe quelle couleur en gris :

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); // Orange atténué, faible saturation
grayscale('#FF5733');      // Gris pur avec la même luminosité

Mélange de deux couleurs

L'interpolation linéaire en espace RGB est le mélange le plus simple :

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 % corail, 30 % blanc → une teinte
mix('#FF5733', '#000000', 0.7); // 70 % corail, 30 % noir → une ombre

Le mélange RGB produit des résultats perceptuellement plats entre les couleurs complémentaires. Pour de meilleurs points médians, des bibliothèques comme chroma.js offrent le mélange en espaces OKLCH ou Lab.

Rapport de contraste (WCAG)

Le rapport de contraste WCAG est calculé à partir de la luminance relative, et non des valeurs brutes des canaux. La luminance requiert une correction gamma — l'inverse de l'encodage appliqué lors du stockage des valeurs sRGB :

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 — ne passe pas WCAG AA pour le texte normal
contrastRatio('#000000', '#FFFFFF'); // 21,0 — contraste maximum

WCAG AA exige 4,5:1 pour le texte normal et 3:1 pour le texte agrandi. Vérifiez toute paire avec le Convertisseur de couleurs.


Comparaison des bibliothèques populaires : chroma.js, culori, tinycolor2

Pour tout ce qui dépasse les ajustements simples, une bibliothèque dédiée fait gagner un temps significatif et évite les bugs de cas limites dans les calculs de couleurs.

chroma.js

Taille : ~13 Ko compressé | Maturité : 2013, activement maintenu | Licence : BSD

chroma.js est la bibliothèque JavaScript de couleurs la plus connue. Son API est fluente et chaînable :

import chroma from 'chroma-js';

// Analyser n'importe quel format
const color = chroma('#FF5733');

// Ajuster
color.darken(1).hex();      // '#D93B10'
color.lighten(1).hex();     // '#FF8066'
color.saturate(0.5).hex();  // '#FF4719'
color.desaturate(0.5).hex();// '#F2653F'

// Convertir
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]

// Mélanger dans différents espaces de couleur
chroma.mix('#FF5733', '#3B82F6', 0.5, 'rgb');   // En RGB (point médian plat)
chroma.mix('#FF5733', '#3B82F6', 0.5, 'oklch'); // En OKLCH (point médian vibrant)

// Génération de gamme
const scale = chroma.scale(['#FFF5F5', '#FF5733', '#7F0000'])
  .mode('oklch')
  .colors(9);

// Contraste
chroma.contrast('#FF5733', '#FFFFFF'); // 3,0

chroma.js est le bon choix quand vous avez besoin d'une bibliothèque complète et bien documentée avec une configuration minimale et un bon support pour le mélange dans les espaces de couleur.

culori

Taille : ~6 Ko compressé (tree-shakeable) | Maturité : 2019, activement maintenu | Licence : MIT

culori est une bibliothèque moderne et fonctionnelle conçue pour le tree-shaking. Chaque opération est une fonction autonome — vous n'importez que ce que vous utilisez :

import { parse, formatHex, oklch, interpolate, formatCss } from 'culori';

// Analyser
const color = parse('#FF5733'); // { mode: 'rgb', r: 1, g: 0.34, b: 0.2 }

// Convertir en OKLCH
const inOklch = oklch(color); // { mode: 'oklch', l: 0.63, c: 0.19, h: 27.5 }

// Ajuster la luminosité
const lighter = { ...inOklch, l: inOklch.l + 0.1 };
formatHex(lighter); // '#FF8469'

// Interpolation pour dégradés/animation
const gradient = interpolate(['#FF5733', '#3B82F6'], 'oklch');
formatCss(gradient(0.5)); // Couleur du point médian au format CSS

// Génération de gamme
import { samples } from 'culori';
const stops = samples(5).map(t => formatHex(gradient(t)));
// ['#FF5733', '#E85E56', '#9B6DE3', '#6186F0', '#3B82F6']

culori opère sur des objets simples avec une propriété mode, ce qui facilite la sérialisation des couleurs, leur stockage dans l'état ou leur envoi sur un réseau. C'est le meilleur choix pour les projets TypeScript modernes où la taille du bundle importe et où le tree-shaking est en jeu.

tinycolor2

Taille : ~5 Ko compressé | Maturité : 2012, stable (moins actif) | Licence : MIT

tinycolor2 est le parseur le plus petit et le plus permissif — il accepte pratiquement n'importe quel format de chaîne de couleur, y compris les noms de couleurs CSS, sans configuration :

import tinycolor from 'tinycolor2';

// Analyser presque tout
tinycolor('red').toHexString();         // '#FF0000'
tinycolor('hsl(11, 100%, 60%)').toHexString(); // '#FF5733'
tinycolor('#F53').toHexString();        // '#FF5533'

// Ajuster
tinycolor('#FF5733').lighten(15).toHexString();    // '#FF8966'
tinycolor('#FF5733').darken(15).toHexString();     // '#C92B04'
tinycolor('#FF5733').desaturate(30).toHexString(); // '#EB6643'
tinycolor('#FF5733').spin(180).toHexString();      // '#33AEff' (complémentaire)

// Lisibilité / contraste
tinycolor.readability('#FF5733', '#FFFFFF'); // 3,0
tinycolor.isReadable('#FF5733', '#FFFFFF');  // false (ne passe pas WCAG AA)
tinycolor.isReadable('#FF5733', '#FFFFFF', { level: 'AA', size: 'large' }); // true

// Harmonies de couleurs
tinycolor('#FF5733').triad();         // [TinyColor, TinyColor, TinyColor]
tinycolor('#FF5733').analogous();     // 6 couleurs analogues
tinycolor('#FF5733').complement();    // Une seule couleur complémentaire

tinycolor2 est le bon choix pour les projets qui ont besoin d'une analyse fiable des chaînes de couleur saisies par l'utilisateur (qui peuvent être dans n'importe quel format) et de manipulations de base sans tirer une dépendance plus lourde.

Récapitulatif de comparaison des bibliothèques

Fonctionnalité chroma.js culori tinycolor2
Taille du bundle ~13 Ko ~6 Ko (tree-shakeable) ~5 Ko
Espaces de couleur RGB, HSL, Lab, LCH, OKLCH 20+ dont OKLCH, P3 RGB, HSL, HSV
Style d'API Fluent/chaînable Fonctionnel Orienté objet
TypeScript Types communautaires Intégré Types communautaires
Mélange de couleurs RGB, HSL, Lab, OKLCH Tout espace de couleur RGB uniquement
Idéal pour Usage complet, viz de données Bundles TS modernes Analyse, opérations simples

Construire un utilitaire de couleur de zéro

Pour une application en production qui n'a besoin que de l'analyse hex, de l'éclaircissement, de l'assombrissement et de la vérification du contraste, un utilitaire sans dépendance est souvent le meilleur choix architectural. Voici une implémentation complète :

// 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';
}

Utilisation :

import { lighten, darken, contrastRatio, bestTextColor } from './color-utils.js';

const brand = '#FF5733';
const hover = darken(brand, 10);       // Plus foncé pour :hover
const light = lighten(brand, 30);      // Teinte claire pour les fonds
const text = bestTextColor(brand);     // Noir ou blanc pour le texte sur la couleur de marque

console.log(contrastRatio(text, brand)); // Doit être ≥ 4,5

Considérations de performance pour les couleurs à l'exécution

Mettre en cache agressivement

Les calculs de couleur sont référentiellement transparents — la même entrée hex produit toujours la même sortie. Un simple mécanisme de mémoïsation évite de recalculer à chaque rendu :

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);

Propriétés personnalisées CSS plutôt que JavaScript

Pour le thème dynamique, ne recalculez pas les couleurs en JavaScript à chaque changement d'état. Calculez la palette une fois, écrivez-la dans des propriétés personnalisées CSS et laissez CSS s'occuper de chaque composant :

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));
}

// Appelé une fois quand la couleur de marque change, pas à chaque rendu
applyTheme('#FF5733');

Éviter d'analyser des chaînes dans les boucles de rendu

Analyser "rgb(255, 87, 51)" dans une fonction de rendu React qui s'exécute 60 fois par seconde est coûteux. Analysez une fois, stockez le résultat, et transmettez des objets de couleur structurés dans votre arbre de composants plutôt que des chaînes brutes.

// Coûteux : analyse à chaque appel
const color = chroma(colorString).darken(1).hex();

// Mieux : analyser une fois, transformer plusieurs fois
const parsed = chroma(colorString);
const darkVariant = parsed.darken(1).hex();
const lightVariant = parsed.lighten(1).hex();

Pour la génération de couleurs à l'exécution à grande échelle — sélecteurs de couleurs se mettant à jour à chaque mouvement de souris, ou visualisations avec des centaines de couleurs calculées — culori est le meilleur choix car son architecture fonctionnelle sans état et sa taille de bundle réduite ont le moins de surcharge par appel.


Points clés

  • JavaScript ne possède pas de type de couleur natif — analysez les chaînes hex, RGB et HSL en objets structurés avant tout calcul.
  • L'arithmétique HSL (ajustement de l pour la luminosité, s pour la saturation) couvre la plupart des transformations de design courantes directement.
  • Le rapport de contraste WCAG nécessite un calcul de luminance avec correction gamma — ne l'approximez pas avec les calculs du canal de luminosité.
  • chroma.js est la bibliothèque la plus complète avec un excellent support des espaces de couleur dont le mélange OKLCH ; culori est le choix moderne tree-shakeable pour les projets TypeScript ; tinycolor2 est le parseur le plus permissif pour les scénarios de saisie utilisateur.
  • Un utilitaire sans dépendance est viable pour les applications qui n'ont besoin que d'analyser, d'éclaircir/assombrir et de vérifier le contraste — l'implémentation complète fait moins de 100 lignes.
  • Mettez agressivement en cache les couleurs calculées et écrivez les résultats dans des propriétés personnalisées CSS plutôt que de recalculer à chaque rendu.
  • Utilisez le Convertisseur de couleurs pour vérifier visuellement toute transformation de couleur, et le Générateur de nuances pour générer des gammes complètes de 50 à 950 compatibles Tailwind à partir d'un seul hex de marque.

Couleurs associées

Marques associées

Outils associés