Skip to content

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

// Color contrast — WCAG 2.2 color contrast math.
// Language: Swift 5.9 — standard library + Foundation (Darwin pow; no third-party color packages).
// 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 Foundation

/// An 8-bit sRGB color triple.
struct Rgb: Equatable {
    var r: Int
    var g: Int
    var b: Int
}

/// 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 `nil` when the string is not a valid
/// hex color, so callers can distinguish "invalid" from a real color.
func hexToRgb(_ color: String) -> Rgb? {
    // Trim surrounding whitespace, then drop exactly one leading '#'
    // (removeFirst takes a single occurrence, not all of them).
    var h = color.trimmingCharacters(in: .whitespaces)
    if h.hasPrefix("#") { h.removeFirst() }

    // Expand CSS shorthand: each hex digit is doubled ("abc" -> "aabbcc").
    let expanded: String
    switch h.count {
    case 3: expanded = h.map { "\($0)\($0)" }.joined()
    case 6: expanded = h
    default: return nil
    }

    // Six validated hex digits always fit in UInt32; bit-slice into channels.
    guard expanded.allSatisfy(\.isHexDigit),
          let n = UInt32(expanded, radix: 16) else {
        return nil
    }
    return Rgb(r: Int((n >> 16) & 0xFF), g: Int((n >> 8) & 0xFF), b: Int(n & 0xFF))
}

/// 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.
func channel(_ c: Int) -> Double {
    let v = Double(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, or `nil` if the
/// hex string is invalid.
func luminance(_ color: String) -> Double? {
    guard let rgb = hexToRgb(color) else { return nil }
    // 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 `nil` 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.
func contrastRatio(_ fg: String, _ bg: String) -> Double? {
    guard let l1 = luminance(fg), let l2 = luminance(bg) else { return nil }
    return (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05)
}

// Demo — run: swift swift.swift
func report(_ fg: String, _ bg: String) {
    let body = contrastRatio(fg, bg).map { String(format: "%.2f", $0) } ?? "invalid"
    print("contrast \(fg) on \(bg) = \(body)")
}

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 →