Skip to content

Color Contrast Checker — Python source

Check WCAG 2.2 contrast ratio between any two colors with AA / AAA pass-fail for normal and large text, plus a live preview. For accessible, on-brand design.

This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.

"""Color contrast — WCAG 2.2 color contrast math.

Language: Python.

CosmoDev polyglot showcase port of the `contrast` tool.
Ported from src/lib/color.ts — functionally equivalent (same inputs -> same outputs).

Display source — part of CosmoDev's polyglot tool pages.
"""
from __future__ import annotations

import re
from typing import Optional

# Six hexadecimal digits, validated after shorthand expansion.
_HEX6 = re.compile(r"^[0-9a-fA-F]{6}$")


def hex_to_rgb(color: str) -> Optional[tuple[int, int, int]]:
    """Parse a CSS-style hex color into an (r, g, b) triple (each 0-255).

    Accepts an optional leading '#', a 3-digit shorthand ('#abc'), or the
    6-digit form ('#aabbcc'). Returns None when the string is not a valid
    hex color, so callers can distinguish "invalid" from a real color.
    """
    # Trim surrounding whitespace, then drop exactly one leading '#'.
    # removeprefix strips a single occurrence (unlike lstrip, which would
    # eat every leading '#' and diverge from CSS).
    h = color.strip().removeprefix("#")

    # Expand CSS shorthand: each hex digit is doubled ('abc' -> 'aabbcc').
    if len(h) == 3:
        h = "".join(c * 2 for c in h)

    if not _HEX6.match(h):
        return None

    return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))


def _channel(c: int) -> float:
    """Linearize a single sRGB channel value (0-255) per WCAG 2.2.

    8-bit color values are gamma-encoded for display; WCAG luminance is
    computed in linear-light space using the inverse sRGB transfer function.
    The small-value branch is the linear segment of that curve.
    """
    v = c / 255
    return v / 12.92 if v <= 0.03928 else ((v + 0.055) / 1.055) ** 2.4


def luminance(color: str) -> Optional[float]:
    """WCAG relative luminance of a hex color on a 0..1 scale.

    Returns None if the hex string is invalid.
    """
    rgb = hex_to_rgb(color)
    if rgb is None:
        return None
    # Rec. 709 luma coefficients for the red/green/blue primaries.
    return 0.2126 * _channel(rgb[0]) + 0.7152 * _channel(rgb[1]) + 0.0722 * _channel(rgb[2])


def contrast_ratio(fg: str, bg: str) -> Optional[float]:
    """WCAG contrast ratio between two hex colors on a 1..21 scale.

    Returns None if either color is invalid.

    The 0.05 offset models the ambient luminance assumed by WCAG, keeping the
    ratio finite (and >= 1) even for identical colors.
    """
    l1 = luminance(fg)
    l2 = luminance(bg)
    if l1 is None or l2 is None:
        return None
    lighter, darker = (l1, l2) if l1 >= l2 else (l2, l1)
    return (lighter + 0.05) / (darker + 0.05)

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →