DevToolBoxฟรี
บล็อก

Color Palette Generator: Create Beautiful Color Schemes — Complete Guide

13 min readโดย DevToolBox

TL;DR

A color palette generator creates harmonious color sets from a base hue using color theory rules — complementary (180°), triadic (120°), tetradic (90°) — combined with tint/shade scales (50–900). Use HSL for algorithmic generation, OKLCH for perceptually uniform CSS mixing, and chroma.js for advanced JavaScript palette logic. Always validate contrast ratios against WCAG AA (4.5:1) for accessible designs. Try our online color picker to build and export palettes instantly.

Color Models: RGB, HSL, HSV/HSB, and HEX Conversions

Every color on screen is ultimately stored as RGB triplets, but different color models serve different purposes. Choosing the right model for algorithmic palette generation dramatically simplifies the math.

Model Comparison

ModelComponentsBest For
RGBR 0-255, G 0-255, B 0-255Screen rendering, pixel manipulation
HEX#RRGGBB or #RGB shorthandCSS, design tokens, storage
HSLH 0-360°, S 0-100%, L 0-100%Color schemes, tint/shade generation
HSV/HSBH 0-360°, S 0-100%, V 0-100%Color pickers, image editing
OKLCHL 0-1, C 0-0.4, H 0-360°Perceptually uniform CSS mixing
// ── HEX ↔ RGB Conversions ──────────────────────────────────────────
function hexToRgb(hex: string): { r: number; g: number; b: number } {
  const clean = hex.replace('#', '');
  // Expand 3-char shorthand #RGB → #RRGGBB
  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),
  };
}

function rgbToHex(r: number, g: number, b: number): string {
  return '#' + [r, g, b]
    .map(v => Math.round(v).toString(16).padStart(2, '0'))
    .join('');
}

// ── RGB ↔ HSL Conversions ──────────────────────────────────────────
function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
  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 delta = max - min;
  let h = 0, s = 0;
  const l = (max + min) / 2;

  if (delta !== 0) {
    s = delta / (1 - Math.abs(2 * l - 1));
    switch (max) {
      case rn: h = ((gn - bn) / delta + 6) % 6; break;
      case gn: h = (bn - rn) / delta + 2;        break;
      case bn: h = (rn - gn) / delta + 4;        break;
    }
    h = Math.round(h * 60);
  }
  return { h, s: Math.round(s * 100), l: Math.round(l * 100) };
}

function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
  const sn = s / 100, ln = l / 100;
  const a = sn * Math.min(ln, 1 - ln);
  const f = (n: number) => {
    const k = (n + h / 30) % 12;
    return ln - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
  };
  return { r: Math.round(f(0) * 255), g: Math.round(f(8) * 255), b: Math.round(f(4) * 255) };
}

// ── RGB ↔ HSV Conversions ──────────────────────────────────────────
function rgbToHsv(r: number, g: number, b: number): { h: number; s: number; v: number } {
  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 delta = max - min;
  let h = 0;
  const s = max === 0 ? 0 : delta / max;
  const v = max;

  if (delta !== 0) {
    switch (max) {
      case rn: h = ((gn - bn) / delta + 6) % 6; break;
      case gn: h = (bn - rn) / delta + 2;        break;
      case bn: h = (rn - gn) / delta + 4;        break;
    }
    h = Math.round(h * 60);
  }
  return { h, s: Math.round(s * 100), v: Math.round(v * 100) };
}

// ── Usage ──────────────────────────────────────────────────────────
const rgb = hexToRgb('#3b82f6');   // { r: 59, g: 130, b: 246 }
const hsl = rgbToHsl(59, 130, 246); // { h: 217, s: 91, l: 60 }
const back = hslToRgb(217, 91, 60); // { r: 59, g: 130, b: 246 }
console.log(rgbToHex(back.r, back.g, back.b)); // "#3b82f6"

CSS Custom Properties: Design Tokens and Color Schemes

CSS custom properties (variables) are the foundation of modern design systems. Defining colors as tokens in :root enables consistent theming, one-line dark mode switches, and runtime updates without rebuilding the stylesheet.

The recommended pattern for design tokens combines semantic naming (what the color means) with HSL values stored as raw channel numbers — this unlocks dynamic alpha channels using the CSS hsl(var(--color-primary) / 0.5) syntax.

/* ── Design Token Architecture ───────────────────────────────────────
   Store raw HSL channels (no units) for alpha compositing:
   Usage: hsl(var(--color-primary) / 0.5)  → 50% transparent primary
   ─────────────────────────────────────────────────────────────────── */

:root {
  /* ── Brand Palette (base hue: 217° — blue) ── */
  --color-primary-h: 217;
  --color-primary-s: 91%;
  --color-primary-l: 60%;
  --color-primary:   var(--color-primary-h) var(--color-primary-s) var(--color-primary-l);

  /* ── Semantic Tokens ── */
  --bg-base:       hsl(0 0% 100%);
  --bg-subtle:     hsl(210 20% 98%);
  --bg-muted:      hsl(210 16% 93%);

  --text-primary:  hsl(215 25% 15%);
  --text-secondary:hsl(215 16% 40%);
  --text-muted:    hsl(215 12% 60%);

  --border-subtle: hsl(215 16% 90%);
  --border-strong: hsl(215 16% 75%);

  /* ── Interactive State Tokens ── */
  --color-primary-hover:  hsl(var(--color-primary-h) var(--color-primary-s) 52%);
  --color-primary-active: hsl(var(--color-primary-h) var(--color-primary-s) 44%);
  --color-primary-subtle: hsl(var(--color-primary-h) var(--color-primary-s) 95%);

  /* ── Semantic Status Colors ── */
  --color-success: hsl(142 72% 29%);
  --color-warning: hsl(38 92% 50%);
  --color-error:   hsl(0 84% 60%);
  --color-info:    hsl(199 89% 48%);

  /* ── Spacing & Radius Tokens (non-color, but part of the design system) ── */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
}

/* ── Component Usage ───────────────────────────────────────────────── */
.btn-primary {
  background-color: hsl(var(--color-primary));
  color: hsl(0 0% 100%);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  transition: background-color 150ms ease;
}

.btn-primary:hover  { background-color: var(--color-primary-hover); }
.btn-primary:active { background-color: var(--color-primary-active); }

/* ── Alpha Variant (same hue, transparent) ──────────────────────── */
.btn-ghost {
  background-color: hsl(var(--color-primary) / 0.1);
  color:            hsl(var(--color-primary));
  border:           1px solid hsl(var(--color-primary) / 0.3);
}

/* ── Programmatic Updates via JavaScript ────────────────────────── */
/* Change entire theme by swapping --color-primary-h:
   document.documentElement.style.setProperty('--color-primary-h', '270');
   → Instantly shifts all primary tokens from blue to purple */

Generating Complementary, Triadic, Tetradic, and Split-Complementary Colors

Color harmony schemes are all derived from rotating the hue angle in HSL space. Because hue is cyclic (0° = 360°), use the modulo operator to keep values in range. Saturation and lightness stay constant to maintain visual balance.

// ── Color Harmony Generator ──────────────────────────────────────
interface HslColor { h: number; s: number; l: number }
interface ColorScheme { name: string; colors: HslColor[] }

function rotateHue(base: HslColor, degrees: number): HslColor {
  return { ...base, h: (base.h + degrees + 360) % 360 };
}

function hslToCss({ h, s, l }: HslColor): string {
  return `hsl(${h}, ${s}%, ${l}%)`;
}

// ── Complementary: 1 base + 1 opposite (180°) ─────────────────────
function complementary(base: HslColor): ColorScheme {
  return {
    name: 'Complementary',
    colors: [base, rotateHue(base, 180)],
  };
}

// ── Triadic: 3 colors evenly spaced at 120° ──────────────────────
function triadic(base: HslColor): ColorScheme {
  return {
    name: 'Triadic',
    colors: [base, rotateHue(base, 120), rotateHue(base, 240)],
  };
}

// ── Tetradic (Square): 4 colors at 90° intervals ─────────────────
function tetradic(base: HslColor): ColorScheme {
  return {
    name: 'Tetradic',
    colors: [
      base,
      rotateHue(base, 90),
      rotateHue(base, 180),
      rotateHue(base, 270),
    ],
  };
}

// ── Split-Complementary: base + 2 colors near complement ─────────
function splitComplementary(base: HslColor): ColorScheme {
  return {
    name: 'Split-Complementary',
    colors: [base, rotateHue(base, 150), rotateHue(base, 210)],
  };
}

// ── Analogous: 5 colors within ±30° (harmonious, low contrast) ───
function analogous(base: HslColor, count = 5, spread = 30): ColorScheme {
  const step = (spread * 2) / (count - 1);
  return {
    name: 'Analogous',
    colors: Array.from({ length: count }, (_, i) =>
      rotateHue(base, -spread + i * step)
    ),
  };
}

// ── Example usage ──────────────────────────────────────────────────
const base: HslColor = { h: 217, s: 91, l: 60 }; // blue
const comp    = complementary(base);       // blue + orange
const tri     = triadic(base);             // blue + red + green
const tet     = tetradic(base);            // blue + purple + orange + yellow
const splitC  = splitComplementary(base);  // blue + red-orange + yellow-orange
const analog  = analogous(base, 5, 30);   // blue family ±30°

// Print CSS values
tri.colors.forEach(c => console.log(hslToCss(c)));
// hsl(217, 91%, 60%)
// hsl(337, 91%, 60%)
// hsl(97, 91%, 60%)

Tint, Shade, and Tone — Generating a 50–900 Scale Like Tailwind

A tint mixes the color with white (increase lightness), a shade mixes with black (decrease lightness), and a tone mixes with gray (decrease saturation). Tailwind CSS uses a 10-step scale (50, 100, 200, … 900) where 500 is the base color.

// ── Tailwind-style 50-900 scale generator ────────────────────────
//    500 = base color; lighter values → tints; darker → shades

type ColorScale = Record<number, string>;

function generateScale(baseH: number, baseS: number, baseL: number): ColorScale {
  // Lightness map: key = Tailwind step, value = target lightness %
  const lightnessMap: Record<number, number> = {
    50:  97,
    100: 94,
    200: 87,
    300: 77,
    400: 67,
    500: baseL,   // ← base color
    600: Math.max(baseL - 10, 20),
    700: Math.max(baseL - 22, 14),
    800: Math.max(baseL - 32, 10),
    900: Math.max(baseL - 42,  7),
  };

  // Saturation boost for mid-range, reduction for extremes
  const saturationMap: Record<number, number> = {
    50:  Math.round(baseS * 0.20),
    100: Math.round(baseS * 0.35),
    200: Math.round(baseS * 0.55),
    300: Math.round(baseS * 0.75),
    400: Math.round(baseS * 0.90),
    500: baseS,
    600: Math.min(baseS + 5, 100),
    700: Math.min(baseS + 8, 100),
    800: Math.min(baseS + 6, 100),
    900: Math.min(baseS + 4, 100),
  };

  const scale: ColorScale = {};
  for (const step of [50, 100, 200, 300, 400, 500, 600, 700, 800, 900]) {
    scale[step] = `hsl(${baseH}, ${saturationMap[step]}%, ${lightnessMap[step]}%)`;
  }
  return scale;
}

// ── Generate blue-500 (#3b82f6) scale ─────────────────────────────
const blueScale = generateScale(217, 91, 60);
console.log(blueScale);
// {
//   50:  "hsl(217, 18%, 97%)",   ← very light blue-white
//   100: "hsl(217, 32%, 94%)",
//   200: "hsl(217, 50%, 87%)",
//   300: "hsl(217, 68%, 77%)",
//   400: "hsl(217, 82%, 67%)",
//   500: "hsl(217, 91%, 60%)",   ← base
//   600: "hsl(217, 96%, 50%)",
//   700: "hsl(217, 99%, 38%)",
//   800: "hsl(217, 97%, 28%)",
//   900: "hsl(217, 95%, 18%)",   ← very dark blue
// }

// ── CSS Custom Properties output ──────────────────────────────────
function scaleToCssVars(name: string, scale: ColorScale): string {
  return Object.entries(scale)
    .map(([step, value]) => `  --color-${name}-${step}: ${value};`)
    .join('\n');
}

console.log(`:root {\n${scaleToCssVars('blue', blueScale)}\n}`);

Color Contrast and Accessibility — WCAG AA/AAA Compliance

The WCAG 2.1 contrast ratio formula compares the relative luminance of two colors. Luminance is computed by first linearizing sRGB values (removing gamma encoding), then combining channels with human-vision weights (green contributes most to perceived brightness).

// ── WCAG 2.1 Contrast Ratio Calculator ───────────────────────────

/** Convert sRGB 0-255 channel to linear light value */
function linearize(channel: number): number {
  const c = channel / 255;
  return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}

/** Relative luminance: 0 (black) → 1 (white) */
function relativeLuminance(r: number, g: number, b: number): number {
  return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
}

/** WCAG contrast ratio: range 1:1 (identical) → 21:1 (black/white) */
function contrastRatio(hex1: string, hex2: string): number {
  const c1 = hexToRgb(hex1);
  const c2 = hexToRgb(hex2);
  const L1 = relativeLuminance(c1.r, c1.g, c1.b);
  const L2 = relativeLuminance(c2.r, c2.g, c2.b);
  const lighter = Math.max(L1, L2);
  const darker  = Math.min(L1, L2);
  return (lighter + 0.05) / (darker + 0.05);
}

type WcagLevel = 'AAA' | 'AA' | 'AA Large' | 'Fail';

function wcagLevel(ratio: number, isLargeText = false): WcagLevel {
  if (ratio >= 7)                       return 'AAA';
  if (ratio >= 4.5)                     return 'AA';
  if (ratio >= 3 && isLargeText)        return 'AA Large';
  return 'Fail';
}

// ── Accessibility audit for a color pair ─────────────────────────
function auditColors(foreground: string, background: string) {
  const ratio = contrastRatio(foreground, background);
  return {
    ratio:        parseFloat(ratio.toFixed(2)),
    normalText:   wcagLevel(ratio, false),
    largeText:    wcagLevel(ratio, true),
    passes:       ratio >= 4.5,
  };
}

// ── Safe color combinations (contrast ≥ 4.5:1) ───────────────────
const palette = {
  blue500:  '#3b82f6',
  blue700:  '#1d4ed8',
  blue900:  '#1e3a8a',
  white:    '#ffffff',
  gray900:  '#111827',
  gray100:  '#f3f4f6',
};

console.log(auditColors(palette.white,    palette.blue700));
// { ratio: 6.84, normalText: 'AA', largeText: 'AAA', passes: true }

console.log(auditColors(palette.blue500,  palette.white));
// { ratio: 3.07, normalText: 'Fail', largeText: 'AA Large', passes: false }
// ⚠ blue-500 on white fails AA for normal text! Use blue-600+ instead.

console.log(auditColors(palette.gray900,  palette.gray100));
// { ratio: 15.32, normalText: 'AAA', largeText: 'AAA', passes: true }

// ── Find the darkest accessible shade for a given background ──────
function findAccessibleShade(
  scale: ColorScale,
  background: string,
  minRatio = 4.5
): string | null {
  for (const step of [900, 800, 700, 600, 500, 400]) {
    const ratio = contrastRatio(scale[step], background);
    if (ratio >= minRatio) return scale[step];
  }
  return null;
}

CSS color-mix(): Native Color Mixing with OKLCH and P3 Gamut

The color-mix() function, supported in all modern browsers since mid-2023 (Chrome 111, Firefox 113, Safari 16.2), enables native CSS color operations without JavaScript. The key decision is the color space used for interpolation — it significantly affects the midpoint color.

OKLCH is the recommended color space: it is perceptually uniform (equal numeric changes produce equal visual changes), avoids the gray mud problem of RGB mixing, and supports the wide-gamut P3 display gamut.

/* ── CSS color-mix() Syntax ──────────────────────────────────────────
   color-mix(in <color-space>, <color1> <percentage>, <color2>)
   If percentages omit, they split 50/50.
   ──────────────────────────────────────────────────────────────────── */

:root {
  --brand:   oklch(62% 0.19 250);   /* vivid blue */
  --accent:  oklch(70% 0.22 140);   /* vivid green */
  --surface: oklch(99% 0.00 0);     /* near-white */

  /* ── Tints: mix brand with white at different ratios ── */
  --brand-50:  color-mix(in oklch, var(--brand)  5%, white);
  --brand-100: color-mix(in oklch, var(--brand) 15%, white);
  --brand-200: color-mix(in oklch, var(--brand) 30%, white);
  --brand-300: color-mix(in oklch, var(--brand) 50%, white);
  --brand-400: color-mix(in oklch, var(--brand) 70%, white);
  /* 500 = base */
  --brand-600: color-mix(in oklch, var(--brand), black 15%);
  --brand-700: color-mix(in oklch, var(--brand), black 30%);
  --brand-800: color-mix(in oklch, var(--brand), black 50%);
  --brand-900: color-mix(in oklch, var(--brand), black 70%);

  /* ── Complementary color via hue rotation ── */
  /* oklch hue is perceptual, not geometric — use chroma.js for precise 180° */
  --complement: oklch(from var(--brand) l c calc(h + 180));

  /* ── Transparency variants ── */
  --brand-ghost:  color-mix(in oklch, var(--brand) 10%, transparent);
  --brand-subtle: color-mix(in oklch, var(--brand) 20%, var(--surface));
}

/* ── P3 Wide-Gamut Colors (requires @media (color-gamut: p3)) ─────── */
@supports (color: color(display-p3 0 0 1)) {
  :root {
    /* P3 can express more saturated colors than sRGB */
    --vivid-blue: color(display-p3 0.10 0.45 0.98);
    --vivid-red:  color(display-p3 0.95 0.10 0.15);
  }
}

/* ── Fallback pattern for older browsers ────────────────────────── */
.btn {
  background: #3b82f6;           /* sRGB fallback */
  background: var(--brand);      /* OKLCH with @supports */
}

/* ── Dark mode variant using color-mix ──────────────────────────── */
@media (prefers-color-scheme: dark) {
  :root {
    /* Lighten brand for dark backgrounds */
    --brand-text: color-mix(in oklch, var(--brand) 70%, white);
    --bg-base:    oklch(18% 0.01 250);
    --bg-subtle:  color-mix(in oklch, var(--bg-base), white 5%);
  }
}

JavaScript chroma.js: Scale Generation, Bezier Interpolation, and Brewer Palettes

chroma.js is a 13KB JavaScript library (no dependencies) that handles color space conversions, palette generation, and perceptually smooth gradients. Install with npm install chroma-js and optionally npm install @types/chroma-js for TypeScript.

// npm install chroma-js @types/chroma-js
import chroma from 'chroma-js';

// ── Basic Color Manipulation ───────────────────────────────────────
const blue = chroma('#3b82f6');
console.log(blue.hex());          // "#3b82f6"
console.log(blue.rgb());          // [59, 130, 246]
console.log(blue.hsl());          // [0.6028, 0.8947, 0.5980]
console.log(blue.lab());          // [52.27, 4.39, -51.84]
console.log(blue.lch());          // [52.27, 51.85, 274.84]

// ── Lightness / Darkness ───────────────────────────────────────────
const lighter = blue.brighten(1).hex(); // "#7eaff9"
const darker  = blue.darken(1).hex();   // "#145bb2"
const tinted  = blue.mix('white', 0.5, 'lch').hex(); // perceptual mix

// ── Scale Generation (10-step palette) ───────────────────────────
const blueScale = chroma.scale(['#dbeafe', '#3b82f6', '#1e3a8a'])
  .mode('lch')       // perceptually uniform interpolation
  .colors(10);       // returns array of 10 hex strings
// ['#dbeafe', '#a5c7f8', '#6aa0f3', '#3f7de8', '#2563d7', ...]

// ── Bezier Interpolation (smoother than linear) ───────────────────
const smooth = chroma.bezier(['#f0f9ff', '#3b82f6', '#1e3a8a'])
  .scale()
  .colors(9);
// Bezier interpolation avoids the "muddy middle" problem

// ── ColorBrewer Palettes (for data visualization) ─────────────────
const sequential = chroma.brewer.Blues;         // 9 blues for choropleth maps
const diverging   = chroma.brewer.RdBu;         // red-white-blue diverging
const qualitative = chroma.brewer.Set3;         // 12 distinct categorical colors
console.log(sequential); // ['#f7fbff', '#deebf7', '#c6dbef', ...]

// ── Contrast Checking ─────────────────────────────────────────────
const ratio = chroma.contrast('#3b82f6', '#ffffff');
console.log(ratio); // 3.07 — fails WCAG AA for normal text

// Find the darkest blue that passes AA on white
const blueShades = blueScale.filter(hex => chroma.contrast(hex, '#fff') >= 4.5);
console.log(blueShades[0]); // First shade meeting AA requirement

// ── Generate Tailwind-compatible palette object ───────────────────
function generateTailwindPalette(baseHex: string): Record<number, string> {
  const base = chroma(baseHex);
  const scale = chroma.scale([
    base.brighten(3).desaturate(0.5).hex(),  // 50
    baseHex,                                  // 500
    base.darken(3).hex(),                     // 900
  ]).mode('lch').colors(10);

  const steps = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
  return Object.fromEntries(steps.map((step, i) => [step, scale[i]]));
}

const myBlue = generateTailwindPalette('#3b82f6');
// { 50: '#eff6ff', 100: '#dbeafe', ..., 900: '#1e3a8a' }

// ── Multi-hue Gradient for Data Visualization ─────────────────────
const heatmap = chroma.scale('YlOrRd').domain([0, 100]);
const getHeatColor = (value: number) => heatmap(value).hex();
console.log(getHeatColor(0));   // '#ffffcc' (yellow)
console.log(getHeatColor(50));  // '#fd8d3c' (orange)
console.log(getHeatColor(100)); // '#800026' (dark red)

Python colorsys Module + Pillow: Extract Dominant Colors from Images

Python's standard library colorsys module provides conversion functions between RGB, HLS, HSV, and YIQ. The Pillow (PIL fork) library handles image loading and pixel data extraction. Combining them with sklearn's K-means algorithm extracts a dominant color palette from any image.

# pip install Pillow scikit-learn numpy

import colorsys
import numpy as np
from PIL import Image
from sklearn.cluster import KMeans

# ── 1. colorsys: Standard-library Color Conversions ──────────────
# All colorsys values are normalized 0.0–1.0

rgb = (0.231, 0.510, 0.965)           # = #3b82f6

# RGB → HLS (note: Hue-Lightness-Saturation order, not H-S-L)
h, l, s = colorsys.rgb_to_hls(*rgb)   # (0.603, 0.598, 0.895)
print(f"Hue: {h*360:.1f}°, Saturation: {s*100:.1f}%, Lightness: {l*100:.1f}%")

# RGB → HSV
h, s, v = colorsys.rgb_to_hsv(*rgb)   # (0.603, 0.760, 0.965)
print(f"Hue: {h*360:.1f}°, Saturation: {s*100:.1f}%, Value: {v*100:.1f}%")

# HLS → RGB (back-conversion)
r, g, b = colorsys.hls_to_rgb(h, l, s)
hex_out  = '#{:02x}{:02x}{:02x}'.format(int(r*255), int(g*255), int(b*255))
print(hex_out)  # "#3b82f6"

# ── 2. Pillow: Load Image and Extract Pixel Data ──────────────────
def extract_dominant_colors(image_path: str, n_colors: int = 6) -> list[str]:
    """Return n_colors dominant HEX colors from an image using K-means."""
    img = Image.open(image_path).convert('RGB')

    # Resize for performance (150×150 = max 22,500 pixels to cluster)
    img.thumbnail((150, 150), Image.LANCZOS)

    # Flatten to (N, 3) array of RGB pixels
    pixels = np.array(img).reshape(-1, 3).astype(float)

    # Remove near-white and near-black pixels (they dominate but aren't interesting)
    mask = (pixels.sum(axis=1) > 50) & (pixels.sum(axis=1) < 700)
    pixels = pixels[mask]

    if len(pixels) < n_colors:
        return []

    # K-means clustering to find dominant color centers
    kmeans = KMeans(n_clusters=n_colors, random_state=42, n_init='auto')
    kmeans.fit(pixels)

    # Sort by cluster size (most dominant first)
    centers = kmeans.cluster_centers_.astype(int)
    counts  = np.bincount(kmeans.labels_)
    order   = np.argsort(-counts)

    return [
        '#{:02x}{:02x}{:02x}'.format(*centers[i])
        for i in order
    ]

# Usage
palette = extract_dominant_colors('photo.jpg', n_colors=5)
print(palette)  # ['#2a4a7a', '#d4a853', '#8cb87f', '#e8dfc9', '#1a2d4b']

# ── 3. Using colorthief (wraps K-means, simpler API) ──────────────
# pip install colorthief
from colorthief import ColorThief

ct = ColorThief('photo.jpg')
dominant = ct.get_color(quality=1)        # (42, 74, 122) RGB tuple
palette2  = ct.get_palette(color_count=6) # list of 6 RGB tuples

hex_palette = ['#{:02x}{:02x}{:02x}'.format(*c) for c in palette2]
print(hex_palette)

# ── 4. Generate Color Scale from Extracted Color ─────────────────
def generate_scale(r: int, g: int, b: int) -> dict:
    """Generate a 9-step tint/shade scale (100-900) from an RGB color."""
    h, l, s = colorsys.rgb_to_hls(r/255, g/255, b/255)
    scale = {}
    lightness_steps = [0.95, 0.88, 0.78, 0.68, l, max(l-0.12,0.08), max(l-0.24,0.06), max(l-0.38,0.04), max(l-0.50,0.02)]
    for i, (step, lt) in enumerate(zip([100,200,300,400,500,600,700,800,900], lightness_steps)):
        rr, gg, bb = colorsys.hls_to_rgb(h, lt, s)
        scale[step] = '#{:02x}{:02x}{:02x}'.format(int(rr*255), int(gg*255), int(bb*255))
    return scale

print(generate_scale(59, 130, 246))
# {100: '#dde8fd', 200: '#b7cefc', ..., 900: '#0d1e3d'}

Tailwind CSS Color Configuration — Extending with Custom Palettes

Tailwind's color system is configured in tailwind.config.js (or tailwind.config.ts). Use theme.extend.colors to add custom palettes while keeping Tailwind defaults. Use theme.colors to replace them entirely.

For runtime-switchable themes (dark mode, white-labeling), use CSS variable references in the config instead of raw color values. This pattern generates Tailwind utility classes that read from CSS variables, enabling theme changes without a rebuild.

// tailwind.config.ts
import type { Config } from 'tailwindcss';

const config: Config = {
  content: ['./src/**/*.{ts,tsx}'],
  darkMode: 'class', // or 'media'
  theme: {
    extend: {
      colors: {
        // ── Static custom palette ──────────────────────────────────
        brand: {
          50:  '#eff6ff',
          100: '#dbeafe',
          200: '#bfdbfe',
          300: '#93c5fd',
          400: '#60a5fa',
          500: '#3b82f6',   // ← base
          600: '#2563eb',
          700: '#1d4ed8',
          800: '#1e40af',
          900: '#1e3a8a',
          950: '#172554',
        },

        // ── CSS-variable-backed palette (runtime theming) ──────────
        // In your CSS: --color-primary: 217 91% 60%;
        // Usage: bg-primary (maps to hsl(var(--color-primary)))
        primary: {
          DEFAULT: 'hsl(var(--color-primary) / <alpha-value>)',
          hover:   'hsl(var(--color-primary-hover) / <alpha-value>)',
          subtle:  'hsl(var(--color-primary-subtle) / <alpha-value>)',
        },
        // Semantic tokens
        surface: {
          DEFAULT: 'hsl(var(--bg-base) / <alpha-value>)',
          subtle:  'hsl(var(--bg-subtle) / <alpha-value>)',
          muted:   'hsl(var(--bg-muted) / <alpha-value>)',
        },
        'content-primary':   'hsl(var(--text-primary) / <alpha-value>)',
        'content-secondary': 'hsl(var(--text-secondary) / <alpha-value>)',
      },

      // ── Custom Border Radius Tokens ────────────────────────────────
      borderRadius: {
        DEFAULT: '0.5rem',
        lg: '0.75rem',
        xl: '1rem',
      },
    },
  },
  plugins: [],
};

export default config;

/* ── Usage in components ──────────────────────────────────────────────
   <button className="bg-brand-500 hover:bg-brand-600 text-white rounded">
     Static palette - Tailwind resolves at build time
   </button>

   <button className="bg-primary text-white hover:bg-primary-hover">
     CSS variable palette - resolves at runtime
   </button>

   <div className="bg-brand-50 text-brand-900 dark:bg-brand-900 dark:text-brand-50">
     Automatic dark mode inversion using Tailwind dark: modifier
   </div>
─────────────────────────────────────────────────────────────────────── */

// ── Script: generate tailwind colors from chroma.js scale ─────────
// (Run in Node.js to auto-generate config)
import chroma from 'chroma-js';

function chromaToTailwindShades(baseHex: string): Record<number, string> {
  const scale = chroma.scale([
    chroma(baseHex).brighten(3).desaturate(0.8).hex(),
    baseHex,
    chroma(baseHex).darken(3).hex(),
  ]).mode('lch').colors(10);

  const steps = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
  return Object.fromEntries(steps.map((step, i) => [step, scale[i]]));
}

// Output: copy-paste into tailwind.config.ts
const result = chromaToTailwindShades('#7c3aed'); // violet-600
console.log(JSON.stringify(result, null, 2));

Dark Mode Palettes: Light/Dark Token Pairs and HSL-Based Theming Strategy

Effective dark mode is not simply inverting colors — it requires a separate set of design tokens optimized for dark backgrounds. The key principles are: reduce saturation slightly (vivid colors appear garish on dark backgrounds), increase lightness of interactive elements (they need to stand out without hurting eyes), and use surface layering (not just one background color).

The recommended architecture uses prefers-color-scheme as the default with a .dark class override, so users can toggle manually while respecting system preference by default.

/* ── Complete Light/Dark Token System ────────────────────────────────

   Architecture:
   1. Define primitive palette (50-900 scale) once
   2. Map semantic tokens to primitives per theme
   3. Components only reference semantic tokens
   ──────────────────────────────────────────────────────────────── */

/* ── Primitive Palette (shared, theme-independent) ─────────────── */
:root {
  /* Blue scale */
  --blue-50:  hsl(214, 100%, 97%);
  --blue-100: hsl(214, 95%,  93%);
  --blue-200: hsl(213, 94%,  87%);
  --blue-400: hsl(213, 94%,  68%);
  --blue-500: hsl(217, 91%,  60%);
  --blue-600: hsl(221, 83%,  53%);
  --blue-700: hsl(224, 76%,  48%);
  --blue-800: hsl(226, 71%,  40%);
  --blue-900: hsl(224, 64%,  33%);

  /* Gray scale */
  --gray-0:   hsl(0, 0%, 100%);
  --gray-50:  hsl(210, 20%, 98%);
  --gray-100: hsl(220, 14%, 96%);
  --gray-200: hsl(220, 13%, 91%);
  --gray-300: hsl(216, 12%, 84%);
  --gray-400: hsl(218, 11%, 65%);
  --gray-500: hsl(220, 9%,  46%);
  --gray-600: hsl(215, 14%, 34%);
  --gray-700: hsl(217, 19%, 27%);
  --gray-800: hsl(215, 28%, 17%);
  --gray-850: hsl(220, 30%, 13%);
  --gray-900: hsl(222, 33%, 11%);
  --gray-950: hsl(224, 39%,  7%);
}

/* ── Light Mode Semantic Tokens ────────────────────────────────── */
:root,
.light {
  /* Backgrounds */
  --bg-canvas:  var(--gray-0);       /* Page background */
  --bg-base:    var(--gray-50);      /* Main content area */
  --bg-subtle:  var(--gray-100);     /* Subtle sections */
  --bg-muted:   var(--gray-200);     /* Disabled, muted areas */
  --bg-overlay: rgba(0, 0, 0, 0.5); /* Modals, drawers */

  /* Text */
  --text-primary:   var(--gray-900);
  --text-secondary: var(--gray-600);
  --text-tertiary:  var(--gray-400);
  --text-disabled:  var(--gray-300);
  --text-inverse:   var(--gray-0);
  --text-link:      var(--blue-600);
  --text-link-hover:var(--blue-700);

  /* Borders */
  --border-subtle: var(--gray-200);
  --border-base:   var(--gray-300);
  --border-strong: var(--gray-400);
  --border-focus:  var(--blue-500);

  /* Interactive */
  --bg-interactive:        var(--blue-600);
  --bg-interactive-hover:  var(--blue-700);
  --bg-interactive-subtle: var(--blue-50);
  --text-on-interactive:   var(--gray-0);

  /* Status */
  --status-success-bg:   hsl(142, 76%, 94%);
  --status-success-text: hsl(142, 72%, 29%);
  --status-error-bg:     hsl(0, 93%, 94%);
  --status-error-text:   hsl(0, 72%, 51%);
  --status-warning-bg:   hsl(38, 100%, 93%);
  --status-warning-text: hsl(25, 95%, 43%);
}

/* ── Dark Mode Semantic Tokens ─────────────────────────────────── */
@media (prefers-color-scheme: dark) { :root { color-scheme: dark; } }

.dark,
@media (prefers-color-scheme: dark) {
  :not([data-theme='light']) {
    /* Backgrounds — layered elevation model */
    --bg-canvas:  var(--gray-950);   /* Base layer */
    --bg-base:    var(--gray-900);   /* Cards, content */
    --bg-subtle:  var(--gray-850);   /* Side panels */
    --bg-muted:   var(--gray-800);   /* Inputs, table rows */
    --bg-overlay: rgba(0, 0, 0, 0.7);

    /* Text — reduced contrast to prevent eye strain */
    --text-primary:   var(--gray-50);
    --text-secondary: var(--gray-400);
    --text-tertiary:  var(--gray-500);
    --text-disabled:  var(--gray-600);
    --text-inverse:   var(--gray-900);
    --text-link:      var(--blue-400);   /* Lighter in dark mode */
    --text-link-hover:var(--blue-300);

    /* Borders */
    --border-subtle: var(--gray-800);
    --border-base:   var(--gray-700);
    --border-strong: var(--gray-600);
    --border-focus:  var(--blue-400);

    /* Interactive — slightly lighter blue on dark */
    --bg-interactive:        var(--blue-500);
    --bg-interactive-hover:  var(--blue-400);
    --bg-interactive-subtle: hsl(217, 91%, 15%);
    --text-on-interactive:   var(--gray-0);

    /* Status — muted backgrounds for dark mode */
    --status-success-bg:   hsl(142, 40%, 13%);
    --status-success-text: hsl(142, 70%, 58%);
    --status-error-bg:     hsl(0, 45%, 13%);
    --status-error-text:   hsl(0, 80%, 67%);
    --status-warning-bg:   hsl(38, 60%, 13%);
    --status-warning-text: hsl(38, 90%, 65%);
  }
}

/* ── Component using semantic tokens (auto adapts to mode) ──────── */
.card {
  background: var(--bg-base);
  border:     1px solid var(--border-subtle);
  color:      var(--text-primary);
  border-radius: 8px;
  padding: 1.5rem;
}

.card-title {
  color: var(--text-primary);
  font-weight: 700;
}

.card-description {
  color: var(--text-secondary);
}

/* ── Next.js / React: Toggle with localStorage ────────────────────
   const toggle = () => {
     const isDark = document.documentElement.classList.toggle('dark');
     localStorage.setItem('theme', isDark ? 'dark' : 'light');
   };
   // On mount: apply saved theme before hydration
   const saved = localStorage.getItem('theme');
   if (saved === 'dark' || (!saved && matchMedia('(prefers-color-scheme: dark)').matches)) {
     document.documentElement.classList.add('dark');
   }
─────────────────────────────────────────────────────────────────────── */

Try It Online

Generate and export color palettes instantly with our free Color Picker Online — pick a base color, get complementary/triadic/tetradic schemes, preview contrast ratios, and copy CSS variables, HEX, HSL, or Tailwind config with one click.

Key Takeaways

  • Use HSL for algorithmic generation: rotating hue by 120°/180° produces color harmonies; adjusting lightness generates tint/shade scales.
  • Store CSS variables as raw HSL channels (217 91% 60% not hsl(217, 91%, 60%)) to enable the hsl(var(--color-primary) / 0.5) alpha syntax.
  • OKLCH is the best CSS color space for color-mix() — it is perceptually uniform and avoids the gray desaturation problem of RGB mixing.
  • WCAG AA requires 4.5:1 contrast for normal text, 3:1 for large text. Blue-500 (#3b82f6) on white is only 3.07:1 — use Blue-600+ for accessible body text.
  • chroma.js LCH/Lab interpolation produces visually smoother gradients than linear RGB or HSL interpolation. Use .mode('lch') on all scales.
  • Pillow + K-means is the standard approach for extracting dominant colors from images; the colorthief library wraps this for convenience.
  • Tailwind CSS variable references (hsl(var(--color-primary) / <alpha-value>)) enable runtime theme switching without rebuilding the stylesheet.
  • Dark mode needs separate saturation tuning: reduce saturation by 10-20% and increase lightness of interactive elements so they read clearly without eye strain.
  • Semantic token naming (--bg-base, --text-primary) decouples components from color values, making global rebrands a single-file change.
  • ColorBrewer palettes (via chroma.brewer) are designed for data visualization and guarantee perceptual distinguishability for categorical or sequential data.
𝕏 Twitterin LinkedIn
บทความนี้มีประโยชน์ไหม?

อัปเดตข่าวสาร

รับเคล็ดลับการพัฒนาและเครื่องมือใหม่ทุกสัปดาห์

ไม่มีสแปม ยกเลิกได้ตลอดเวลา

ลองเครื่องมือที่เกี่ยวข้อง

🎨Color Picker Online🌈CSS Gradient Generator🎨Color Converter

บทความที่เกี่ยวข้อง

คู่มือ CSS Flexbox ฉบับสมบูรณ์: จากพื้นฐานถึงรูปแบบขั้นสูง

เชี่ยวชาญ CSS Flexbox ด้วยคู่มือฉบับสมบูรณ์ครอบคลุมคุณสมบัติ container, alignment และรูปแบบทั่วไป

String Case Converter: Convert camelCase, snake_case, kebab-case Online — Complete Guide

Convert between camelCase, PascalCase, snake_case, kebab-case, and more. Complete guide with JavaScript change-case library, Python humps, regex patterns, and TypeScript template literal types.

HTML Escape / Unescape: Encode Special Characters Online — Complete Guide

Escape and unescape HTML, URLs, JSON, SQL, and shell strings. Complete guide covering XSS prevention, HTML entities, URL encoding, JSON escaping, SQL injection prevention, and regex escaping.