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.
