Skip to content

Color Contrast Checker — C++ 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 C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Color contrast — WCAG 2.2 color contrast math.
// Language: C++17 — standard library only (<cmath>, <optional>, <string>).
// Source: CosmoDev polyglot showcase port of the `contrast` tool — canonical
// src/lib/color.ts + this tool's python.py / rust.rs (same inputs -> same outputs).
// License: display source — part of CosmoDev's polyglot tool pages.

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <optional>
#include <string>

// An 8-bit sRGB color triple.
struct Rgb {
    std::uint8_t r, g, b;
};

// Value of a single hex digit, or -1 when the character is not hex.
static int hexval(char c) {
    if (c >= '0' && c <= '9') return c - '0';
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
    return -1;
}

// Parses a CSS-style hex color into an Rgb value.
//
// Accepts an optional leading '#', a 3-digit shorthand ("#abc"), or the
// 6-digit form ("#aabbcc"). Returns std::nullopt when the string is not a
// valid hex color, so callers can distinguish "invalid" from a real color.
std::optional<Rgb> hex_to_rgb(const std::string& color) {
    // Trim surrounding whitespace, then drop exactly one leading '#'
    // (a single occurrence, not all of them — matches CSS).
    static const char* WS = " \t\r\n";
    std::size_t first = color.find_first_not_of(WS);
    if (first == std::string::npos) return std::nullopt;
    std::size_t last = color.find_last_not_of(WS);
    std::string h = color.substr(first, last - first + 1);
    if (!h.empty() && h.front() == '#') h.erase(0, 1);

    std::string expanded;
    if (h.size() == 3) {
        // Expand CSS shorthand: each hex digit is doubled ("abc" -> "aabbcc").
        expanded.reserve(6);
        for (char c : h) expanded.append(2, c);
    } else if (h.size() == 6) {
        expanded = h;
    } else {
        return std::nullopt;
    }

    // Six validated hex digits: two per channel, high nibble first.
    int channels[3];
    for (std::size_t i = 0; i < 6; i += 2) {
        int hi = hexval(expanded[i]);
        int lo = hexval(expanded[i + 1]);
        if (hi < 0 || lo < 0) return std::nullopt;
        channels[i / 2] = hi * 16 + lo;
    }
    return Rgb{static_cast<std::uint8_t>(channels[0]),
               static_cast<std::uint8_t>(channels[1]),
               static_cast<std::uint8_t>(channels[2])};
}

// Linearizes a single sRGB channel (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.
static double channel(std::uint8_t c) {
    double v = c / 255.0;
    return v <= 0.03928 ? v / 12.92 : std::pow((v + 0.055) / 1.055, 2.4);
}

// WCAG relative luminance of a hex color on a 0..1 scale, or std::nullopt
// if the hex string is invalid.
std::optional<double> luminance(const std::string& color) {
    std::optional<Rgb> rgb = hex_to_rgb(color);
    if (!rgb) return std::nullopt;
    // Rec. 709 luma coefficients for the red/green/blue primaries.
    return 0.2126 * channel(rgb->r) + 0.7152 * channel(rgb->g) + 0.0722 * channel(rgb->b);
}

// WCAG contrast ratio between two hex colors on a 1..21 scale, or
// std::nullopt 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.
std::optional<double> contrast_ratio(const std::string& fg, const std::string& bg) {
    std::optional<double> l1 = luminance(fg);
    std::optional<double> l2 = luminance(bg);
    if (!l1 || !l2) return std::nullopt;
    double lighter = std::max(*l1, *l2);
    double darker = std::min(*l1, *l2);
    return (lighter + 0.05) / (darker + 0.05);
}

// Demo — build: c++ -std=c++17 cpp.cpp && ./a.out
static void report(const std::string& fg, const std::string& bg) {
    if (auto ratio = contrast_ratio(fg, bg))
        std::cout << "contrast " << fg << " on " << bg << " = " << *ratio << '\n';
    else
        std::cout << "contrast " << fg << " on " << bg << " = invalid\n";
}

int main() {
    std::cout << std::fixed << std::setprecision(2);
    report("#000000", "#ffffff"); // 21.00
    report("#767676", "#ffffff"); // 4.54
    report("nope", "#ffffff");    // invalid
    return 0;
}

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 →