Skip to main content

How to create a 5-color palette where EVERY color is readable against EVERY other color with Python

· 6 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

Creating a color palette where every color is readable against every other color is a high-level design challenge. As the number of colors in your palette increases, the "contrast space" shrinks significantly.

In this article, we’ll build a script that uses an iterative "Collision-Check" algorithm. It generates a candidate color, checks it against every color already in the palette, and only keeps it if it passes the WCAG AA threshold against all of them.


🏗️ The Logic: How the Generator Works

To ensure a palette of 5 colors ($C_1, C_2, C_3, C_4, C_5$) is fully accessible, we have to satisfy 10 unique pair combinations.

The algorithm follows these steps:

  1. Seed: Pick a random starting color.
  2. Generate: Create a random candidate color.
  3. Validate: Check the contrast of the candidate against all existing colors in the palette.
  4. Repeat: If it fails any check, discard and try again. If it passes, add it to the set.
  5. Backtrack: If we can't find a match after 500 tries, start the whole palette over (sometimes the first few colors "block" all remaining possibilities).

💻 The Implementation

I have combined the luminance math from our previous session with the new iterative logic into a single, copy-pasteable script.

# 🎨 Python Script: The "Safe-Set" Accessible Palette Generator

### 1. Requirements
```bash
pip install webcolors

2. The Code

import random
import webcolors

def get_luminance(rgb_tuple):
"""Calculates relative luminance from an RGB tuple."""
rgb_list = [c / 255 for c in rgb_tuple]
linear_rgb = []
for c in rgb_list:
if c <= 0.03928:
linear_rgb.append(c / 12.92)
else:
linear_rgb.append(((c + 0.055) / 1.055) ** 2.4)
return 0.2126 * linear_rgb[0] + 0.7152 * linear_rgb[1] + 0.0722 * linear_rgb[2]

def get_contrast_ratio(rgb1, rgb2):
l1 = get_luminance(rgb1)
l2 = get_luminance(rgb2)
if l1 < l2:
l1, l2 = l2, l1
return (l1 + 0.05) / (l2 + 0.05)

def generate_random_rgb():
return (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))

def generate_safe_palette(size=5, min_ratio=4.5):
"""Generates a palette where EVERY pair meets the min_ratio."""
palette = []
attempts = 0
max_total_attempts = 1000 # Restart palette if stuck

while len(palette) < size:
candidate = generate_random_rgb()

# Check against all existing colors in the palette
is_safe = True
for color in palette:
if get_contrast_ratio(candidate, color) < min_ratio:
is_safe = False
break

if is_safe:
palette.append(candidate)
attempts = 0 # Reset attempts for next color
else:
attempts += 1

# If we can't find a color that fits, the palette is too restricted. Restart.
if attempts > 500:
palette = []
attempts = 0
max_total_attempts -= 1
if max_total_attempts == 0:
return "Error: Could not find a valid palette for this ratio."

return [webcolors.rgb_to_hex(c) for c in palette]

# --- Execution ---
print("🚀 Generating a 5-color palette (WCAG AA Compliant)...")
safe_set = generate_safe_palette(size=5, min_ratio=4.5)

if isinstance(safe_set, list):
print(f"\n✅ Success! Here is your accessible palette:")
for i, hex_code in enumerate(safe_set):
print(f"Color {i+1}: {hex_code.upper()}")
else:
print(safe_set)


📊 The "Combinatorial Explosion" Problem

Why is it so hard to get 5 colors to pass? Every new color you add adds $N-1$ new constraints.

Palette SizeContrast Pairs to CheckDifficulty Level
2 Colors1Very Easy
3 Colors3Easy
5 Colors10Hard
10 Colors45Nearly Impossible (at 4.5:1 ratio)

🛡️ Practical Tips for Design

The "Light/Dark" Anchor Strategy: To make a 5-color palette work, it is usually best to "anchor" the palette with one very dark color (near black) and one very light color (near white). This opens up the "middle ground" for the other three colors to exist without colliding with each other.


📚 Sources & Technical Refs

  • [1.1] W3C: WCAG 2.1 Contrast Ratio Standard - The source for the 4.5:1 ratio requirement.
  • [2.1] Colormind.io: API Documentation - How deep-learning models are sometimes used to predict "pleasing" but safe palettes.
  • [3.1] Python Docs: Random Module - Efficiency of pseudo-random number generation for large-scale sampling.

More on python