Color Contrast Checker — Zig 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 Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Color contrast — WCAG 2.2 color contrast math.
// Language: Zig 0.13 — standard library only (std.math.pow).
// 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.
const std = @import("std");
/// An 8-bit sRGB color triple.
const Rgb = struct {
r: u8,
g: u8,
b: u8,
};
/// Value of a single hex digit, or null when the byte is not hex.
fn hexDigit(c: u8) ?u4 {
return switch (c) {
'0'...'9' => @intCast(c - '0'),
'a'...'f' => @intCast(c - 'a' + 10),
'A'...'F' => @intCast(c - 'A' + 10),
else => null,
};
}
/// 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 `error.InvalidHex` when the string is
/// not a valid hex color, so callers can distinguish "invalid" from a real
/// color.
fn hexToRgb(color: []const u8) error{InvalidHex}!Rgb {
// Trim surrounding whitespace, then drop exactly one leading '#'
// (a single occurrence, not all of them — matches CSS).
var s = std.mem.trim(u8, color, " \t\r\n");
if (s.len > 0 and s[0] == '#') s = s[1..];
// Expand CSS shorthand: each hex digit is doubled ("abc" -> "aabbcc").
var digits: [6]u8 = undefined;
if (s.len == 3) {
for (s, 0..) |c, i| {
digits[i * 2] = c;
digits[i * 2 + 1] = c;
}
} else if (s.len == 6) {
@memcpy(&digits, s);
} else {
return error.InvalidHex;
}
// Six validated hex digits: two per channel, high nibble first.
var channels: [3]u8 = undefined;
var i: usize = 0;
while (i < 6) : (i += 2) {
const hi = hexDigit(digits[i]) orelse return error.InvalidHex;
const lo = hexDigit(digits[i + 1]) orelse return error.InvalidHex;
channels[i / 2] = (@as(u8, hi) << 4) | @as(u8, lo);
}
return .{ .r = channels[0], .g = channels[1], .b = 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.
fn channel(c: u8) f64 {
const v = @as(f64, @floatFromInt(c)) / 255.0;
return if (v <= 0.03928) v / 12.92 else std.math.pow(f64, (v + 0.055) / 1.055, 2.4);
}
/// WCAG relative luminance of a hex color on a 0..1 scale, or
/// `error.InvalidHex` if the hex string is invalid.
fn luminance(color: []const u8) error{InvalidHex}!f64 {
const rgb = try hexToRgb(color);
// 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
/// `error.InvalidHex` 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.
fn contrastRatio(fg: []const u8, bg: []const u8) error{InvalidHex}!f64 {
const l1 = try luminance(fg);
const l2 = try luminance(bg);
return (@max(l1, l2) + 0.05) / (@min(l1, l2) + 0.05);
}
// Demo — run: zig run zig.zig
fn report(fg: []const u8, bg: []const u8) void {
const stdout = std.io.getStdOut().writer();
if (contrastRatio(fg, bg)) |ratio| {
stdout.print("contrast {s} on {s} = {d:.2}\n", .{ fg, bg, ratio }) catch {};
} else |_| {
stdout.print("contrast {s} on {s} = invalid\n", .{ fg, bg }) catch {};
}
}
pub fn main() void {
report("#000000", "#ffffff"); // 21.00
report("#767676", "#ffffff"); // 4.54
report("nope", "#ffffff"); // invalid
}
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 →