Contrast Checker: How to Calculate Color Contrast in Python
Designing a beautiful UI is pointless if half your users can't read it. Whether it's a person with a visual impairment or someone trying to check their phone on a sunny day, color contrast is the secret sauce of accessible design.
The WCAG (Web Content Accessibility Guidelines) provides a mathematical way to ensure text stands out against its background. Let's integrate a "Contrast Checker" into our Python toolkit.
📐 The Math of "Readability"
To calculate contrast, we first have to calculate Relative Luminance. Humans perceive green as much brighter than blue, so we use a weighted formula based on the sRGB color space.
1. Gamma Correction
Before calculating luminance, we must "linearize" the RGB values (which are usually stored in a non-linear way).
For each color $C \in \{R, G, B\}$:
$$C_{srgb} = \frac{C_{8bit}}{255}$$
$$C_{linear} = \begin{cases} \frac{C_{srgb}}{12.92}, & \text{if } C_{srgb} \leq 0.03928 \\ \left(\frac{C_{srgb} + 0.055}{1.055}\right)^{2.4}, & \text{otherwise} \end{cases}$$
2. The Luminance Formula
Once linearized, we apply the weights:
$$L = 0.2126 \cdot R_{linear} + 0.7152 \cdot G_{linear} + 0.0722 \cdot B_{linear}$$
3. The Contrast Ratio
Finally, the ratio is calculated by comparing the lighter color ($L_1$) and the darker color ($L_2$):
$$CR = \frac{L_1 + 0.05}{L_2 + 0.05}$$
This ratio will be a number between 1:1 (no contrast) and 21:1 (maximum contrast, like black on white).
💻 Python Implementation
This code takes two colors (HEX or Name) and tells you exactly how they perform against accessibility standards.
import webcolors
def get_luminance(color_input):
"""Calculates relative luminance of a color."""
# Convert input to RGB
if color_input.startswith('#'):
rgb = webcolors.hex_to_rgb(color_input)
else:
rgb = webcolors.name_to_rgb(color_input)
# Linearize and Gamma Correct
rgb_list = [rgb.red / 255, rgb.green / 255, rgb.blue / 255]
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)
# Calculate weighted luminance
r, g, b = linear_rgb
return 0.2126 * r + 0.7152 * g + 0.0722 * b
def check_contrast(foreground, background):
l1 = get_luminance(foreground)
l2 = get_luminance(background)
# Ensure l1 is the lighter color
if l1 < l2:
l1, l2 = l2, l1
ratio = (l1 + 0.05) / (l2 + 0.05)
print(f"--- 👓 Accessibility Report ---")
print(f"Colors: {foreground} on {background}")
print(f"Ratio : {round(ratio, 2)}:1")
# Check against WCAG 2.1 Standards
results = {
"AA (Normal Text)": ratio >= 4.5,
"AA (Large Text)": ratio >= 3.0,
"AAA (Normal Text)": ratio >= 7.0,
"AAA (Large Text)": ratio >= 4.5
}
for test, passed in results.items():
status = "✅ PASS" if passed else "❌ FAIL"
print(f"{test.ljust(18)}: {status}")
# --- Test It ---
check_contrast("white", "tomato")
print("\n")
check_contrast("#2c3e50", "#ecf0f1")
📊 Understanding the WCAG Grades
The WCAG uses a grading system (AA and AAA) to define how accessible a design is.
| Level | Ratio | Target | Use Case |
|---|---|---|---|
| AA (Large) | 3:1 | Text > 18pt | The bare minimum for bold headers. |
| AA (Normal) | 4.5:1 | Body Text | The standard for most professional websites. |
| AAA (Large) | 4.5:1 | Text > 18pt | High accessibility for headlines. |
| AAA (Normal) | 7:1 | Body Text | The "Gold Standard" for maximum readability. |
Note: If you are building a Dark Mode for your app, aim for at least AA (Normal). While absolute black and white
($21:1$)is safe, it can actually cause eye strain (halation) for some users. A dark grey like#121212with off-white text is often more comfortable.
📚 Sources & Technical Refs
- [1.1] W3C: WCAG 2.1 Contrast Ratio Definition - The official documentation for the formula.
- [2.1] WebAIM: Contrast and Color Accessibility - A great interactive resource to test your manual math.
- [3.1] Python Docs: Math Module - For handling power functions in gamma correction.
