A lightweight, zero-dependency Python library for color manipulation.
Project description
🎨 ColorBrew
A lightweight, zero-dependency Python library for color manipulation, conversion, and accessibility analysis.
from colorbrew import Color
brand = Color("#3498db")
brand.lighten(20).hex # "#8ac4ea"
brand.with_alpha(0.8).css_rgb_modern() # "rgb(52 152 219 / 0.8)"
brand.meets_aa(Color("white")) # True
brand.suggest_text_color() # Color('#000000')
brand.scale()[500] # Color('#2986c7')
Why ColorBrew?
- Zero dependencies — pure Python, nothing to install.
- One object, every format — hex, RGB, HSL, HSV, CMYK, Lab, CSS named colors. Parse any, convert to all.
- Alpha channel — first-class support for transparency in hex, CSS, and all transforms.
- CSS Color Level 4 — parse and output modern
rgb(52 152 219 / 0.8)syntax. - Immutable & hashable — safe to use as dict keys and in sets.
- WCAG accessibility — contrast ratios, AA/AAA checks, auto text color, accessible color finder.
- Perceptual color science — CIE L*a*b*, CIEDE2000, Lab-space gradients.
- Designer tools — palettes, blend modes, shade scales, color temperature, colorblind simulation.
- Fully typed — ships with
py.typedmarker; all parameters useLiteraltypes for autocomplete.
Installation
pip install colorbrew
Requires Python 3.10+.
Quick Start
Creating Colors
from colorbrew import Color
# From hex (3, 4, 6, or 8 digits)
c = Color("#3498db")
c = Color("#3498db80") # with alpha
# From RGB integers
c = Color(52, 152, 219)
# From CSS strings (legacy and modern)
c = Color("rgb(52, 152, 219)")
c = Color("rgb(52 152 219 / 0.8)")
c = Color("hsl(204 70% 53%)")
c = Color("hsla(204, 70%, 53%, 0.5)")
# From CSS named colors
c = Color("cornflowerblue")
# From other color spaces
c = Color.from_hsl(204, 70, 53)
c = Color.from_hsv(204, 76, 86)
c = Color.from_cmyk(76, 31, 0, 14)
c = Color.from_lab(61.0, -3.4, -38.3)
# From built-in palettes
c = Color.from_tailwind("sky-500")
c = Color.from_material("blue-600")
c = Color.from_name("steelblue")
# Random
c = Color.random()
Converting Between Formats
Every property returns the same color in a different format — input format doesn't matter:
c = Color("#3498db")
c.rgb # (52, 152, 219)
c.hex # "#3498db"
c.hsl # (204, 70, 53)
c.hsv # (204, 76, 86)
c.cmyk # (76, 31, 0, 14)
c.lab # (61.0, -3.4, -38.3)
c.r, c.g, c.b # 52, 152, 219
CSS Output
c = Color("#3498db")
# Legacy syntax
c.css_rgb # "rgb(52, 152, 219)"
c.css_hsl # "hsl(204, 70%, 53%)"
c.css_rgba(0.5) # "rgba(52, 152, 219, 0.5)"
c.css_hsla(0.5) # "hsla(204, 70%, 53%, 0.5)"
# Modern CSS Color Level 4 syntax
c.css_rgb_modern() # "rgb(52 152 219)"
c.with_alpha(0.8).css_rgb_modern() # "rgb(52 152 219 / 0.8)"
c.css_hsl_modern() # "hsl(204 70% 53%)"
# Format strings
f"{c:hex}" # "#3498db"
f"{c:rgb}" # "rgb(52, 152, 219)"
f"{c:hsl}" # "hsl(204, 70%, 53%)"
Alpha Channel
Full transparency support — parsed from input, preserved through transforms, output in CSS:
# Parse alpha from any format
c = Color("#3498db80") # 8-digit hex
c = Color("rgba(52, 152, 219, 0.5)") # legacy CSS
c = Color("rgb(52 152 219 / 0.5)") # modern CSS
c.alpha # 0.5
c.rgba # (52, 152, 219, 0.5)
c.hex # "#3498db80"
# Modify alpha
c.with_alpha(0.3) # new Color with alpha 0.3
c.opaque # new Color with alpha 1.0
# Transforms preserve alpha
c.lighten(20).alpha # 0.5 (unchanged)
c.complementary().alpha # 0.5 (unchanged)
# Mix interpolates alpha
Color("#ff000080").mix(Color("#0000ff"), 0.5).alpha # 0.75
Color Manipulation
All methods return new Color instances — nothing is mutated:
c = Color("#3498db")
# Lightness & saturation
c.lighten(20) # increase lightness by 20%
c.darken(10) # decrease lightness by 10%
c.saturate(15) # increase saturation by 15%
c.desaturate(25) # decrease saturation by 25%
# Hue
c.rotate(180) # shift hue by 180 degrees
c.complementary() # shortcut for rotate(180)
# Special transforms
c.invert() # RGB inverse (255 - each channel)
c.grayscale() # remove saturation
# Mixing
c.mix(Color("red"), 0.5) # 50/50 blend in RGB space
c.shade(0.3) # mix with black (darken)
c.tint(0.3) # mix with white (lighten)
c.tone(0.3) # mix with gray (mute)
Gradients
Generate smooth color ramps between two colors:
# RGB interpolation (default)
c.gradient(Color("red"), steps=7)
# Lab interpolation (perceptually uniform)
c.gradient(Color("red"), steps=7, space="lab")
Lab-space gradients avoid the muddy midpoints you get with RGB interpolation — colors stay vibrant through the transition.
Palette Generation
Color Harmonies
c = Color("#3498db")
c.complementary() # 1 color — opposite hue (180°)
c.analogous() # 3 colors — neighboring hues (30° apart)
c.analogous(n=5, step=15) # customize count and spacing
c.triadic() # 2 colors — 120° intervals
c.split_complementary() # 2 colors — flanking the complement
c.tetradic() # 3 colors — 90° intervals
Shade Scales
Generate Tailwind-like 50–950 shade scales from any color:
scale = Color("#3498db").scale()
scale[50] # lightest
scale[500] # mid-tone (closest to original)
scale[950] # darkest
# Use in a design system
for step, color in scale.items():
print(f"--brand-{step}: {color.hex};")
Output:
--brand-50: #e2f0f9;
--brand-100: #cde5f5;
--brand-200: #a5d0ee;
--brand-300: #6db4e4;
--brand-400: #3e9cdb;
--brand-500: #2986c7;
--brand-600: #226fa6;
--brand-700: #1b5885;
--brand-800: #133c5c;
--brand-900: #0e2d46;
--brand-950: #091c2c;
Reverse Name Lookup
Find the closest named color from built-in palettes:
match = Color("#3498db").closest_name()
match.name # "dodgerblue"
match.hex # "#1e90ff"
match.distance # 42.94
match.exact # False
# Tailwind and Material Design lookups
Color("#3498db").closest_tailwind() # NameMatch("sky-500", ...)
Color("#3498db").closest_material() # NameMatch("blue-400", ...)
# Use perceptual distance for better accuracy
Color("#3498db").closest_name(method="ciede2000")
Available palettes: 148 CSS named colors, 264 Tailwind CSS colors, 210 Material Design colors.
Accessibility (WCAG 2.1)
Contrast Checking
bg = Color("#1a1a2e")
text = Color("#e0e0e0")
bg.contrast(text) # 12.72 (contrast ratio 1:1 to 21:1)
bg.meets_aa(text) # True (≥ 4.5:1)
bg.meets_aaa(text) # True (≥ 7:1)
bg.meets_aa(text, large=True) # True (large text: ≥ 3:1)
Auto Text Color
Choose black or white text for maximum readability on any background:
bg = Color("#3498db")
bg.suggest_text_color() # Color('#000000') — black is more readable
bg = Color("#1a1a2e")
bg.suggest_text_color() # Color('#ffffff') — white is more readable
Accessible Color Finder
Have a brand color that doesn't pass contrast? Find the closest shade that does:
bg = Color("#ffffff")
brand = Color("#99ccff") # too light for white bg
# Find the closest color to brand that passes AA
accessible = bg.find_accessible_color(brand, level="aa")
accessible.hex # darker shade that passes 4.5:1
# AAA level
accessible = bg.find_accessible_color(brand, level="aaa")
Luminance & Light/Dark Detection
c = Color("#3498db")
c.luminance # 0.29 (WCAG relative luminance)
c.is_light # False
c.is_dark # True
Perceptual Color Distance
Compare colors using human-perception-aware algorithms:
a = Color("#3498db")
b = Color("#2ecc71")
# CIEDE2000 — best perceptual accuracy (default)
a.distance(b) # 34.18
# CIE76 — faster, less accurate
a.distance(b, method="cie76") # 44.57
# Euclidean RGB — simple, not perceptual
a.distance(b, method="euclidean") # 158.69
Standalone Functions
from colorbrew import rgb_to_lab, lab_to_rgb, delta_e_76, delta_e_2000
lab = rgb_to_lab(52, 152, 219) # (61.0, -3.4, -38.3)
rgb = lab_to_rgb(61.0, -3.4, -38.3) # (52, 152, 219)
delta_e_2000(lab1, lab2) # perceptual distance
delta_e_76(lab1, lab2) # CIE76 distance
Color Blindness Simulation
Preview how colors appear to users with color vision deficiencies:
c = Color("#ff4444")
c.simulate_colorblind("protanopia") # red-blind
c.simulate_colorblind("deuteranopia") # green-blind
c.simulate_colorblind("tritanopia") # blue-blind
Uses Viénot/Brettel simulation matrices — industry standard for accessibility testing.
Color Temperature
c = Color("#ff6b35")
c.temperature # "warm"
c.kelvin # estimated color temperature in Kelvin (1000–40000)
Color("#3498db").temperature # "cool"
Color("#808080").temperature # "neutral"
Blend Modes
Photoshop-style blend modes for compositing:
base = Color("#3498db")
top = Color("#e74c3c")
base.blend(top, "multiply")
base.blend(top, "screen")
base.blend(top, "overlay")
base.blend(top, "soft_light")
base.blend(top, "hard_light")
base.blend(top, "difference")
Standalone Converter Functions
For cases where you don't need the full Color class:
from colorbrew import (
hex_to_rgb, rgb_to_hex,
hsl_to_rgb, rgb_to_hsl,
hsv_to_rgb, rgb_to_hsv,
cmyk_to_rgb, rgb_to_cmyk,
rgb_to_lab, lab_to_rgb,
)
rgb_to_hex(52, 152, 219) # "#3498db"
hex_to_rgb("#3498db") # (52, 152, 219)
rgb_to_hsl(255, 0, 0) # (0, 100, 50)
hsl_to_rgb(0, 100, 50) # (255, 0, 0)
rgb_to_lab(52, 152, 219) # (61.0, -3.4, -38.3)
lab_to_rgb(61.0, -3.4, -38.3) # (52, 152, 219)
Real-World Use Cases
Building a Design System
from colorbrew import Color
primary = Color("#3498db")
scale = primary.scale()
css_vars = "\n".join(
f" --primary-{step}: {color.hex};"
for step, color in scale.items()
)
print(f":root {{\n{css_vars}\n}}")
Accessible UI Components
def button_styles(bg_hex: str) -> dict:
bg = Color(bg_hex)
text = bg.suggest_text_color()
hover = bg.darken(10)
border = bg.darken(20)
return {
"background": bg.hex,
"color": text.hex,
"hover_bg": hover.hex,
"border": border.hex,
"contrast_ratio": bg.contrast(text),
}
Checking Brand Color Accessibility
brand = Color("#ff6b35")
bg = Color("#ffffff")
print(f"Contrast ratio: {brand.contrast(bg):.1f}:1")
print(f"AA pass: {brand.meets_aa(bg)}")
print(f"AAA pass: {brand.meets_aaa(bg)}")
if not brand.meets_aa(bg):
fixed = bg.find_accessible_color(brand)
print(f"Suggested fix: {fixed.hex} (ratio: {fixed.contrast(bg):.1f}:1)")
Generating a Perceptual Gradient for Data Visualization
start = Color("#3498db")
end = Color("#e74c3c")
# Lab-space gradient stays vibrant (no muddy browns)
gradient = start.gradient(end, steps=10, space="lab")
colors = [c.hex for c in gradient]
Colorblind-Safe Palette Validation
palette = [Color("#e74c3c"), Color("#2ecc71"), Color("#3498db")]
for deficiency in ["protanopia", "deuteranopia", "tritanopia"]:
simulated = [c.simulate_colorblind(deficiency) for c in palette]
for i, (a, b) in enumerate(zip(simulated, simulated[1:])):
dist = a.distance(b)
if dist < 10:
print(f"Warning: colors {i} and {i+1} are too similar "
f"under {deficiency} (ΔE={dist:.1f})")
Development
uv sync
uv run pytest
uv run ruff check src/
Contributing
Contributions are welcome! Please open an issue first to discuss what you'd like to change.
Changelog
See Releases for a full list of changes.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file colorbrew-0.9.0.tar.gz.
File metadata
- Download URL: colorbrew-0.9.0.tar.gz
- Upload date:
- Size: 58.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d891da2dd929ab3ae65653b7629b32cd63c4b721b46e450d581570e606b8c994
|
|
| MD5 |
14c0330b6f1a77309cca18912d7c37a5
|
|
| BLAKE2b-256 |
6cc808969dad146d3ad0f23d7690c2bcc800c64c3f3dc3bb7c4de46daa7f9d0e
|
Provenance
The following attestation bundles were made for colorbrew-0.9.0.tar.gz:
Publisher:
publish.yml on zfoq/colorbrew
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
colorbrew-0.9.0.tar.gz -
Subject digest:
d891da2dd929ab3ae65653b7629b32cd63c4b721b46e450d581570e606b8c994 - Sigstore transparency entry: 1058141327
- Sigstore integration time:
-
Permalink:
zfoq/colorbrew@01623f6f4e2fad7307d5cac9b05ae98bdee42a1a -
Branch / Tag:
refs/tags/0.9.0 - Owner: https://github.com/zfoq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@01623f6f4e2fad7307d5cac9b05ae98bdee42a1a -
Trigger Event:
push
-
Statement type:
File details
Details for the file colorbrew-0.9.0-py3-none-any.whl.
File metadata
- Download URL: colorbrew-0.9.0-py3-none-any.whl
- Upload date:
- Size: 39.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
acc44a4b81173294ebf11f21335ebf963da7c8f4b5a64dd021970e706d267436
|
|
| MD5 |
3c7342fbf10d27d779d63cc0bf58ce89
|
|
| BLAKE2b-256 |
24512dff5e9c3593cbec09840053c8e57c45fe751c76b713d3fe578b1208b804
|
Provenance
The following attestation bundles were made for colorbrew-0.9.0-py3-none-any.whl:
Publisher:
publish.yml on zfoq/colorbrew
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
colorbrew-0.9.0-py3-none-any.whl -
Subject digest:
acc44a4b81173294ebf11f21335ebf963da7c8f4b5a64dd021970e706d267436 - Sigstore transparency entry: 1058141328
- Sigstore integration time:
-
Permalink:
zfoq/colorbrew@01623f6f4e2fad7307d5cac9b05ae98bdee42a1a -
Branch / Tag:
refs/tags/0.9.0 - Owner: https://github.com/zfoq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@01623f6f4e2fad7307d5cac9b05ae98bdee42a1a -
Trigger Event:
push
-
Statement type: