Introduction
Color theory is fundamental to design, art, and user interface development. Understanding how to generate harmonious color palettes programmatically empowers developers to create tools that help designers work more efficiently. In this article, we'll explore the mathematical algorithms behind common color palette generation techniques, all implemented using the HSL (Hue, Saturation, Lightness) color space.
Why HSL?
HSL is the preferred color space for color manipulation because:
- Intuitive: Hue represents the pure color, Saturation represents color intensity, and Lightness represents brightness
- Mathematically simple: Color relationships can be calculated using simple arithmetic on the Hue component
- Perceptually relevant: Changes in HSL values produce predictable visual results
The HSL Color Wheel
Core Color Algorithms
1. Adjacent Colors (Analogous)
Concept: Colors that are next to each other on the color wheel. These create harmonious, cohesive palettes.
Mathematical Formula:
Given: base hue (H), number of colors (n), angle step (α)
For each color i from 0 to n-1:
hue_i = H + α × (i - center)
Where center = floor(n / 2) to center the base color
Implementation:
analogous(n?: number, angle?: number): Color[] {
n = n || 3
if (n < 3) n = 3
if (n > 8) n = 8
angle = angle || 15
// Predefined angle offsets to center the base color
let angles: number[] = [
angle * -4, // -60°
angle * -3, // -45°
angle * -2, // -30°
angle * -1, // -15°
0, // 0° (base color)
angle * 1, // +15°
angle * 2, // +30°
angle * 3, // +45°
angle * 4, // +60°
]
// Start index mapping based on number of colors
// For 3 colors: use indices [3, 4, 5] → [-15°, 0°, +15°]
// For 4 colors: use indices [3, 4, 5, 6] → [-15°, 0°, +15°, +30°]
let angleStartIndex = [-1, -1, -1, 3, 3, 2, 2, 1, 1, 0, 0]
let analogous: Color[] = []
let hsl = this.toHsl()
let i = 0
while (i < n) {
let newH = hsl.h + angles[angleStartIndex[n] + i]
// Normalize hue to 0-360 range
if (newH > 360) {
newH = newH % 360
} else if (newH < 0) {
newH = newH % 360
newH = 360 + newH
}
let hslString = `hsl(${newH},${hsl.s},${hsl.l})`
analogous.push(new Color(hslString))
i++
}
return analogous
}
Example:
- Base color: Red (0°)
- 5 colors, 15° step: [345°, 0°, 15°, 30°, 45°]
- Result: Deep red → Red → Orange-red → Orange → Yellow-orange
2. Complementary Colors
Concept: Colors that are opposite each other on the color wheel (180° apart). They create maximum contrast and vibrant looks.
Mathematical Formula:
complementary_hue = (base_hue + 180) % 360
Implementation:
complementary(): Color {
let hsl = this.toHsl()
let newH = hsl.h + 180
if (newH > 360) {
newH -= 360
}
let hslString = `hsl(${newH},${hsl.s},${hsl.l})`
return new Color(hslString)
}
Color Wheel Positions:
Red (0°) ↔ Cyan (180°)
Orange (30°) ↔ Azure (210°)
Yellow (60°) ↔ Blue (240°)
Green (120°) ↔ Magenta (300°)
3. Split Complementary
Concept: Takes a base color and the two colors adjacent to its complement. Offers high contrast with less tension than pure complementary.
Mathematical Formula:
complement = base_hue + 180
split_colors = analogous(complement, n, angle)
// Replace complement with base color in results
Implementation:
splitComplementary(n?: number, angle?: number) {
let complementary = this.complementary()
// Get analogous colors of the complement
let splitComplementary: Color[] = complementary.analogous(n, angle)
// Replace the complementary color with the base color
return splitComplementary.map((e) => {
if (e.toHsl().h == complementary.toHsl().h) {
return this
}
return e
})
}
Example:
- Base: Red (0°)
- Complement: Cyan (180°)
- Split (±15°): [165°, 180°, 195°]
- Final palette: [165° (Teal), 0° (Red), 195° (Sky Blue)]
4. Triadic Colors
Concept: Three colors equally spaced around the color wheel (120° apart). Creates balanced, vibrant palettes.
Mathematical Formula:
For i = 0, 1, 2:
hue_i = (base_hue + i × 120) % 360
Implementation:
triadic(angle?: number) {
angle = angle || 120
return this.analogous(3, angle)
}
Classic Triadic Combinations:
Primary triad: Red (0°), Yellow (120°), Blue (240°)
Secondary triad: Orange (30°), Green (150°), Purple (270°)
5. Tetradic Colors (Rectangle/Square)
Concept: Four colors arranged in a rectangle on the color wheel. Two complementary pairs.
Mathematical Formula:
For rectangle with angle α:
color1: base_hue
color2: base_hue + α
color3: base_hue + 180
color4: base_hue + 180 + α
Implementation:
tetradic(angle?: number) {
let analogous = this.analogous(3, angle)
return [
analogous[0], // Base - angle
this, // Base
analogous[0].complementary(), // (Base - angle) + 180
this.complementary(), // Base + 180
]
}
Visual Representation:
6. Shades (Dark Variants)
Concept: Variations of a color by decreasing lightness. Creates depth and shadow effects.
Mathematical Formula:
For i = 1 to n:
lightness_i = base_lightness - (i × step)
minimum lightness = 10%
Implementation:
shades(n?: number): Color[] {
n = n || 5
if (n < 1) n = 1
let hsl = this.toHsl()
let shades: Color[] = []
let step = 5 // 5% lightness decrease per step
let i = 1
let light = hsl.l * 100 - step
while (i++ <= n && light >= 10) {
let hslString = `hsl(${hsl.h},${hsl.s},${light / 100})`
shades.push(new Color(hslString))
light -= step
}
return shades.reverse() // Darkest first
}
Lightness Progression:
Base: 50% lightness
Shade 1: 45% (-5%)
Shade 2: 40% (-10%)
Shade 3: 35% (-15%)
Shade 4: 30% (-20%)
Shade 5: 25% (-25%)
7. Tints (Light Variants)
Concept: Variations of a color by increasing lightness. Creates highlights and softer versions.
Mathematical Formula:
For i = 1 to n:
lightness_i = base_lightness + (i × step)
maximum lightness = 90%
Implementation:
tints(n?: number): Color[] {
n = n || 5
if (n < 1) n = 1
let hsl = this.toHsl()
let tints: Color[] = []
let step = 5 // 5% lightness increase per step
let i = 1
let light = hsl.l * 100 + step
while (i++ <= n && light <= 90) {
let hslString = `hsl(${hsl.h},${hsl.s},${light / 100})`
tints.push(new Color(hslString))
light += step
}
return tints // Lightest last
}
8. Monochromatic Colors
Concept: Combines shades and tints of a single hue. Creates elegant, cohesive designs.
Mathematical Formula:
total_colors = n
shades_count = floor((n - 1) / 2)
tints_count = (n - 1) - shades_count
palette = [shades] + [base] + [tints]
Implementation:
monochromatic(n: number) {
n = n || 3
if (n < 3) n = 3
if (n > 8) n = 8
let shdesSize = Math.floor((n - 1) / 2)
let tintsSize = n - 1 - shdesSize
return [...this.shades(shdesSize), this, ...this.tints(tintsSize)]
}
Example for n=5:
[Shade 2, Shade 1, Base, Tint 1, Tint 2]
[40%, 45%, 50%, 55%, 60% lightness]
9. Tones (Saturation Variants)
Concept: Variations of a color by adjusting saturation while keeping hue and lightness constant. Ranges from gray to vivid.
Mathematical Formula:
For each color i:
saturation_i = base_saturation + offset_i
Where offsets range from -20% to +20% in 5% steps
Implementation:
tones(n?: number) {
n = n || 3
if (n < 3) n = 3
if (n > 8) n = 8
let step = 5 // 5% saturation change
let angles: number[] = [
step * -4, // -20%
step * -3, // -15%
step * -2, // -10%
step * -1, // -5%
0, // 0% (base)
step * 1, // +5%
step * 2, // +10%
step * 3, // +15%
step * 4, // +20%
]
let angleStartIndex = [-1, -1, -1, 3, 3, 2, 2, 1, 1, 0, 0]
let tones: Color[] = []
let hsl = this.toHsl()
let i = 0
while (i < n) {
let newS = hsl.s * 100 + angles[angleStartIndex[n] + i]
// Clamp saturation to valid range
newS = Math.max(0, Math.min(100, newS))
let hslString = `hsl(${hsl.h},${newS / 100},${hsl.l})`
tones.push(new Color(hslString))
i++
}
return tones
}
Saturation Progression:
Base: 80% saturation
Tone -2: 70% (muted)
Tone -1: 75% (slightly muted)
Base: 80%
Tone +1: 85% (more vivid)
Tone +2: 90% (most vivid)
Visual Components
Color Wheel Visualization
const ColorWheel = ({ baseColor, colors, title }: ColorWheelProps) => {
const size = 200
const center = size / 2
const radius = size / 2 - 20
// Convert hue to Cartesian coordinates
const getPosition = (hue: number, r: number = radius) => {
// Subtract 90° to start from top (12 o'clock position)
const angle = (hue - 90) * (Math.PI / 180)
return {
x: center + r * Math.cos(angle),
y: center + r * Math.sin(angle),
}
}
// Render color wheel segments
const generateWheelGradient = () => {
const segments = []
const segmentCount = 36
for (let i = 0; i < segmentCount; i++) {
const hue = (i / segmentCount) * 360
segments.push(
<path
key={i}
d={`M ${center} ${center} L ${getPosition(hue, radius).x} ${getPosition(hue, radius).y}
A ${radius} ${radius} 0 0 1 ${getPosition(hue + 360 / segmentCount, radius).x} ${getPosition(hue + 360 / segmentCount, radius).y} Z`}
fill={`hsl(${hue}, 100%, 50%)`}
opacity={0.3}
/>
)
}
return segments
}
}
Monochromatic Visualizer
Shows lightness variations on a vertical gradient bar:
const MonochromaticVisualizer = ({ baseColor, colors }) => {
const baseHsl = baseColor.toHsl()
return (
<div className="relative h-48 w-12 overflow-hidden rounded-lg border">
{/* Gradient background */}
<div
style={{
background: `linear-gradient(to bottom,
hsl(${baseHsl.h}, ${baseHsl.s * 100}%, 90%),
hsl(${baseHsl.h}, ${baseHsl.s * 100}%, 50%),
hsl(${baseHsl.h}, ${baseHsl.s * 100}%, 10%))`,
}}
/>
{/* Position markers */}
{colors.map((color, index) => {
const hsl = color.toHsl()
const lightness = hsl.l * 100
const position = 90 - lightness * 0.8 // Invert: lighter = top
return (
<div
key={index}
className="absolute left-1/2 -translate-x-1/2 rounded-full"
style={{
backgroundColor: color.toHexString(),
top: `${position}%`,
}}
/>
)
})}
</div>
)
}
Complete Algorithm Flow
Practical Applications
UI Design System
// Generate a complete design system
const baseColor = new Color('#3B82F6') // Blue
const designSystem = {
primary: baseColor.toHexString(),
shades: baseColor.shades(4), // Darker variants
tints: baseColor.tints(4), // Lighter variants
analogous: baseColor.analogous(3), // Related colors
complementary: baseColor.complementary(), // Accent color
}
Color Grouping
// Group colors by their closest RYB wheel color
const groupColors = (palette: Color[], wheel: Color[]) => {
const grouper = new ColorGrouper(wheel)
grouper.process(palette)
return grouper.groupedColors
}
Performance Optimizations
Caching Strategy
class Color {
private _cache: Map<string, any> = new Map()
private _cacheEnabled: boolean = true
private getCached<T>(key: string, compute: () => T): T {
if (!this._cacheEnabled) return compute()
if (this._cache.has(key)) return this._cache.get(key)
const value = compute()
this._cache.set(key, value)
return value
}
}
Conclusion
Color palette generation is a perfect blend of mathematical precision and artistic intuition. By understanding the HSL color space and the relationships between hues, we can programmatically create harmonious color schemes that follow established color theory principles.
Key takeaways:
- Hue relationships define color harmony (analogous, complementary, triadic)
- Lightness variations create depth (shades and tints)
- Saturation adjustments control vividness (tones)
- Combining these produces comprehensive design systems
Try It Yourself
Explore color palettes and experiment with these algorithms:
Generate harmonious color schemes for your projects using these mathematical color relationships!
Built with ❤️ using TypeScript, HSL color theory, and React.







Top comments (0)