Skip to content

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

// Color contrast — WCAG 2.2 color contrast math.
// Language: Java 17 — standard library only (java.lang.Math, java.util.Optional).
// 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.

import java.util.Locale;
import java.util.Optional;

/**
 * Color contrast — WCAG 2.2 color contrast math.
 *
 * <p>CosmoDev polyglot showcase port of the {@code contrast} tool. Ported from
 * src/lib/color.ts — functionally equivalent (same inputs -&gt; same outputs).
 */
class Contrast {

    /** An 8-bit sRGB color triple. */
    record Rgb(int r, int g, int b) {}

    /**
     * Parses a CSS-style hex color into an {@link Rgb} value.
     *
     * <p>Accepts an optional leading {@code #}, a 3-digit shorthand ({@code #abc}),
     * or the 6-digit form ({@code #aabbcc}). Returns {@link Optional#empty()} when
     * the string is not a valid hex color, so callers can distinguish "invalid"
     * from a real color.
     */
    static Optional<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.substring(1);

        String expanded;
        switch (h.length()) {
            case 3 -> {
                // Expand CSS shorthand: each hex digit is doubled ("abc" -> "aabbcc").
                StringBuilder sb = new StringBuilder(6);
                for (int i = 0; i < 3; i++) sb.append(h.charAt(i)).append(h.charAt(i));
                expanded = sb.toString();
            }
            case 6 -> expanded = h;
            default -> {
                return Optional.empty();
            }
        }

        if (!expanded.matches("[0-9a-fA-F]{6}")) return Optional.empty();
        int n = Integer.parseInt(expanded, 16);
        return Optional.of(new Rgb((n >> 16) & 0xFF, (n >> 8) & 0xFF, n & 0xFF));
    }

    /**
     * Linearizes a single sRGB channel (0-255) per WCAG 2.2.
     *
     * <p>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 : Math.pow((v + 0.055) / 1.055, 2.4);
    }

    /**
     * WCAG relative luminance of a hex color on a 0..1 scale, or empty if the
     * hex string is invalid.
     */
    static Optional<Double> luminance(String color) {
        return hexToRgb(color).map(rgb ->
                // Rec. 709 luma coefficients for the red/green/blue primaries.
                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 empty if
     * either color is invalid.
     *
     * <p>The 0.05 offset models the ambient luminance assumed by WCAG, keeping
     * the ratio finite (and &gt;= 1) even for identical colors.
     */
    static Optional<Double> contrastRatio(String fg, String bg) {
        Optional<Double> l1 = luminance(fg);
        Optional<Double> l2 = luminance(bg);
        if (l1.isEmpty() || l2.isEmpty()) return Optional.empty();
        double lighter = Math.max(l1.get(), l2.get());
        double darker = Math.min(l1.get(), l2.get());
        return Optional.of((lighter + 0.05) / (darker + 0.05));
    }

    // Demo — run: java java.java
    static void report(String fg, String bg) {
        String body = contrastRatio(fg, bg)
                .map(r -> String.format(Locale.ROOT, "%.2f", r))
                .orElse("invalid");
        System.out.println("contrast " + fg + " on " + bg + " = " + body);
    }

    public static void main(String[] args) {
        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 →