Color Shades Generator: From Linear Interpolation to Tailwind Export#

Design systems need a complete color palette, not just one primary color. Designers often say “give me variations of this blue,” but manually adjusting colors is inefficient. Let’s talk about generating color shades, tints, and tones programmatically.

Three Types of Color Variations#

First, let’s clarify the definitions:

  • Shades: Interpolate toward black - colors get darker
  • Tints: Interpolate toward white - colors get lighter
  • Tones: Interpolate toward gray - colors get more muted

The difference lies in the target color, but the algorithm is the same.

Core Algorithm: RGB Linear Interpolation#

The most straightforward approach is linear interpolation in RGB space:

function hexToRgb(hex: string): [number, number, number] {
  const h = hex.replace('#', '')
  if (h.length === 3) {
    // Handle shorthand like #ABC
    return [
      parseInt(h[0] + h[0], 16),
      parseInt(h[1] + h[1], 16),
      parseInt(h[2] + h[2], 16)
    ]
  }
  return [
    parseInt(h.substring(0, 2), 16),
    parseInt(h.substring(2, 4), 16),
    parseInt(h.substring(4, 6), 16)
  ]
}

function rgbToHex(r: number, g: number, b: number): string {
  const toHex = (n: number) =>
    Math.max(0, Math.min(255, Math.round(n)))
      .toString(16)
      .padStart(2, '0')
  return `#${toHex(r)}${toHex(g)}${toHex(b)}`
}

function interpolate(
  c1: [number, number, number],
  c2: [number, number, number],
  ratio: number
): [number, number, number] {
  return [
    Math.round(c1[0] + (c2[0] - c1[0]) * ratio),
    Math.round(c1[1] + (c2[1] - c1[1]) * ratio),
    Math.round(c1[2] + (c2[2] - c1[2]) * ratio),
  ]
}

The interpolate function is the core: given two colors and a ratio, calculate the intermediate color.

Generating swatches:

function generateSwatches(hex: string) {
  const base = hexToRgb(hex)
  const shades = []
  const tints = []
  const tones = []

  for (let i = 0; i <= 10; i++) {
    const ratio = i / 10
    // Shade: interpolate toward black
    shades.push(rgbToHex(...interpolate(base, [0, 0, 0], ratio)))
    // Tint: interpolate toward white
    tints.push(rgbToHex(...interpolate(base, [255, 255, 255], ratio)))
    // Tone: interpolate toward neutral gray
    tones.push(rgbToHex(...interpolate(base, [128, 128, 128], ratio)))
  }

  return { shades, tints, tones }
}

From #3B82F6 (Tailwind blue-500), this generates 11 levels from 0% (original) to 100% (target).

The Problem with RGB Interpolation#

RGB linear interpolation has a flaw: intermediate colors can look “muddy”.

For example, interpolating from pure red #FF0000 to pure green #00FF00 passes through #808000 (dark yellow) instead of bright yellow. This happens because RGB space is perceptually non-uniform.

A better approach is interpolating in HSL space:

function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
  r /= 255; g /= 255; b /= 255
  const max = Math.max(r, g, b), min = Math.min(r, g, b)
  let h = 0, s = 0, l = (max + min) / 2

  if (max !== min) {
    const d = max - min
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
    switch (max) {
      case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break
      case g: h = ((b - r) / d + 2) / 6; break
      case b: h = ((r - g) / d + 4) / 6; break
    }
  }
  return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)]
}

However, in practice, RGB interpolation works fine for generating Shades/Tints/Tones because the target colors (black/white/gray) have meaningless H and S values anyway. HSL only shows clear advantages when interpolating between two saturated colors.

Exporting Design System Configs#

After generating swatches, make them usable for designers and developers. Three common formats:

CSS Variables#

function exportCssVars(swatches) {
  const lines = [`:root {`]
  swatches.shades.forEach(s => {
    lines.push(`  --color-shade-${s.ratio}: ${s.hex};`)
  })
  swatches.tints.forEach(s => {
    lines.push(`  --color-tint-${s.ratio}: ${s.hex};`)
  })
  lines.push(`}`)
  return lines.join('\n')
}

Output:

:root {
  --color-shade-0: #3b82f6;
  --color-shade-10: #3675dd;
  --color-shade-20: #3168c4;
  /* ... */
  --color-tint-0: #3b82f6;
  --color-tint-10: #4e8ff7;
  --color-tint-20: #619cf8;
  /* ... */
}

Tailwind Config#

function exportTailwind(baseColor: string, swatches) {
  const lines = ['module.exports = { theme: { extend: { colors: {']
  const name = baseColor.replace('#', '')

  lines.push(`  '${name}': '${baseColor}',`)
  swatches.shades.forEach(s => {
    lines.push(`  '${name}-shade-${s.ratio}': '${s.hex}',`)
  })
  swatches.tints.forEach(s => {
    lines.push(`  '${name}-tint-${s.ratio}': '${s.hex}',`)
  })
  lines.push('} } } }')
  return lines.join('\n')
}

JSON Format#

Easy for backend or other tools to consume:

{
  "base": "#3B82F6",
  "shade-0": "#3b82f6",
  "shade-10": "#3675dd",
  "tint-0": "#3b82f6",
  "tint-10": "#4e8ff7"
}

Edge Cases#

1. 3-Digit Shorthand HEX#

#ABC needs to expand to #AABBCC:

if (h.length === 3) {
  return [
    parseInt(h[0] + h[0], 16),
    parseInt(h[1] + h[1], 16),
    parseInt(h[2] + h[2], 16)
  ]
}

2. Value Overflow#

rgbToHex must enforce boundaries:

const toHex = (n: number) =>
  Math.max(0, Math.min(255, Math.round(n)))

While linear interpolation theoretically won’t overflow, this guard provides safety.

3. Random Color Generation#

Add a random button for interactivity:

function randomHex(): string {
  return '#' + Math.floor(Math.random() * 16777215)
    .toString(16)
    .padStart(6, '0')
}

16777215 is 0xFFFFFF, the maximum value in 24-bit color space.

Real-World Use Cases#

  1. Design System Building: Quickly generate complete palettes from brand colors
  2. Dark Mode Adaptation: Map the same hue’s variations across themes
  3. Accessibility Checks: Filter combinations meeting WCAG contrast standards
  4. Code Generation: Export Tailwind configs developers can use directly

Based on these ideas, I built: Color Shades Generator

Features:

  • Real-time Shade/Tint/Tone generation
  • Click to copy any color
  • Auto-display HEX/RGB/HSL formats
  • One-click export to CSS variables/JSON/Tailwind config

Simple code, but solves real problems in design system workflows. Hope this helps.


Related: Color Palette Generator | Color Contrast Checker