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: C11 — standard library only (<math.h> pow; link with -lm).
 * 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 <ctype.h>
#include <math.h>
#include <stdio.h>
#include <string.h>

/* An 8-bit sRGB color triple. */
typedef struct {
    int r, g, b;
} Rgb;

/* 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 0 when the string is not a valid hex
 * color, so callers can distinguish "invalid" from a real color.
 */
int hex_to_rgb(const char *color, Rgb *out) {
    /* Trim surrounding whitespace, then drop exactly one leading '#'
     * (a single occurrence, not all of them — matches CSS). */
    const char *start = color;
    const char *end = color + strlen(color);
    while (start < end && isspace((unsigned char)*start)) start++;
    while (end > start && isspace((unsigned char)end[-1])) end--;
    if (start < end && *start == '#') start++;

    size_t len = (size_t)(end - start);
    char expanded[7];
    if (len == 3) {
        /* Expand CSS shorthand: each hex digit is doubled ("abc" -> "aabbcc"). */
        for (size_t i = 0; i < 3; i++) {
            expanded[i * 2] = start[i];
            expanded[i * 2 + 1] = start[i];
        }
        expanded[6] = '\0';
    } else if (len == 6) {
        memcpy(expanded, start, 6);
        expanded[6] = '\0';
    } else {
        return 0;
    }

    /* Six validated hex digits: two per channel, high nibble first. */
    int channels[3];
    for (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 0;
        channels[i / 2] = hi * 16 + lo;
    }
    out->r = channels[0];
    out->g = channels[1];
    out->b = channels[2];
    return 1;
}

/* 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(int c) {
    double v = c / 255.0;
    return v <= 0.03928 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
}

/* WCAG relative luminance of a hex color on a 0..1 scale.
 * Returns 0 when the hex string is invalid (output untouched then).
 */
int luminance(const char *color, double *out) {
    Rgb rgb;
    if (!hex_to_rgb(color, &rgb)) return 0;
    /* Rec. 709 luma coefficients for the red/green/blue primaries. */
    *out = 0.2126 * channel(rgb.r) + 0.7152 * channel(rgb.g) + 0.0722 * channel(rgb.b);
    return 1;
}

/* WCAG contrast ratio between two hex colors on a 1..21 scale.
 * Returns 0 when 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.
 */
int contrast_ratio(const char *fg, const char *bg, double *out) {
    double l1, l2;
    if (!luminance(fg, &l1) || !luminance(bg, &l2)) return 0;
    double lighter = l1 >= l2 ? l1 : l2;
    double darker = l1 >= l2 ? l2 : l1;
    *out = (lighter + 0.05) / (darker + 0.05);
    return 1;
}

/* Demo — build: cc -std=c11 c.c -lm && ./a.out */
static void report(const char *fg, const char *bg) {
    double ratio;
    if (contrast_ratio(fg, bg, &ratio))
        printf("contrast %s on %s = %.2f\n", fg, bg, ratio);
    else
        printf("contrast %s on %s = invalid\n", fg, bg);
}

int main(void) {
    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 →