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 →