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# 12 / .NET 8 — standard library only (System.Math, Uri.IsHexDigit).
// 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.

using System;
using System.Globalization;
using System.Linq;

// An 8-bit sRGB color triple.
readonly record struct Rgb(byte R, byte G, byte B);

static class Contrast
{
    // 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 null when the string is not a valid
    // hex color, so callers can distinguish "invalid" from a real color.
    static Rgb? HexToRgb(string color)
    {
        // Trim surrounding whitespace, then drop exactly one leading '#'
        // (a single occurrence, not all of them — matches CSS).
        string h = color.Trim();
        if (h.StartsWith('#')) h = h[1..];

        string expanded;
        if (h.Length == 3)
        {
            // Expand CSS shorthand: each hex digit is doubled ("abc" -> "aabbcc").
            expanded = string.Concat(h.Select(c => new string(c, 2)));
        }
        else if (h.Length == 6)
        {
            expanded = h;
        }
        else
        {
            return null;
        }

        foreach (char c in expanded)
        {
            if (!Uri.IsHexDigit(c)) return null;
        }
        return new Rgb(
            Convert.ToByte(expanded.Substring(0, 2), 16),
            Convert.ToByte(expanded.Substring(2, 2), 16),
            Convert.ToByte(expanded.Substring(4, 2), 16));
    }

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

    // WCAG relative luminance of a hex color on a 0..1 scale, or null if the
    // hex string is invalid.
    static double? Luminance(string color)
    {
        Rgb? rgb = HexToRgb(color);
        if (rgb is null) return null;
        Rgb v = rgb.Value;
        // Rec. 709 luma coefficients for the red/green/blue primaries.
        return 0.2126 * Channel(v.R) + 0.7152 * Channel(v.G) + 0.0722 * Channel(v.B);
    }

    // WCAG contrast ratio between two hex colors on a 1..21 scale, or null
    // 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.
    static double? ContrastRatio(string fg, string bg)
    {
        double? l1 = Luminance(fg);
        double? l2 = Luminance(bg);
        if (l1 is null || l2 is null) return null;
        double lighter = Math.Max(l1.Value, l2.Value);
        double darker = Math.Min(l1.Value, l2.Value);
        return (lighter + 0.05) / (darker + 0.05);
    }

    // Demo — build: dotnet run (console project) or csc csharp.cs && ./csharp
    static void Report(string fg, string bg)
    {
        string body = ContrastRatio(fg, bg) is double ratio
            ? ratio.ToString("F2", CultureInfo.InvariantCulture)
            : "invalid";
        Console.WriteLine($"contrast {fg} on {bg} = {body}");
    }

    static void Main()
    {
        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 →