Skip to main content

Contrast Checker: How to Calculate Color Contrast in Python

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

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.

LevelRatioTargetUse Case
AA (Large)3:1Text > 18ptThe bare minimum for bold headers.
AA (Normal)4.5:1Body TextThe standard for most professional websites.
AAA (Large)4.5:1Text > 18ptHigh accessibility for headlines.
AAA (Normal)7:1Body TextThe "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 #121212 with off-white text is often more comfortable.


πŸ“š Sources & Technical Refs​

More on python