Skip to content

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

# Color contrast — WCAG 2.2 color contrast math.
# Language: Ruby 3.2 — standard library only (core classes; no gems).
# 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.

# Parses a CSS-style hex color into an [r, g, b] triple (each 0-255).
#
# 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.
def hex_to_rgb(color)
  # Trim surrounding whitespace, then drop exactly one leading '#'
  # (delete_prefix strips a single occurrence, not all of them).
  h = color.strip.delete_prefix('#')

  # Expand CSS shorthand: each hex digit is doubled ('abc' -> 'aabbcc').
  h = h.chars.flat_map { |c| [c, c] }.join if h.length == 3

  return nil unless h.match?(/\A[0-9a-fA-F]{6}\z/)

  [h[0, 2].to_i(16), h[2, 2].to_i(16), h[4, 2].to_i(16)]
end

# Linearize a single sRGB channel value (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.
def channel(c)
  v = c / 255.0
  v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055)**2.4
end

# WCAG relative luminance of a hex color on a 0..1 scale.
#
# Returns nil if the hex string is invalid.
def luminance(color)
  rgb = hex_to_rgb(color)
  return nil if rgb.nil?

  # Rec. 709 luma coefficients for the red/green/blue primaries.
  0.2126 * channel(rgb[0]) + 0.7152 * channel(rgb[1]) + 0.0722 * channel(rgb[2])
end

# WCAG contrast ratio between two hex colors on a 1..21 scale.
#
# Returns 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.
def contrast_ratio(fg, bg)
  l1 = luminance(fg)
  l2 = luminance(bg)
  return nil if l1.nil? || l2.nil?

  lighter, darker = l1 >= l2 ? [l1, l2] : [l2, l1]
  (lighter + 0.05) / (darker + 0.05)
end

# Demo — run: ruby ruby.rb
def report(fg, bg)
  ratio = contrast_ratio(fg, bg)
  body = ratio.nil? ? 'invalid' : format('%.2f', ratio)
  puts "contrast #{fg} on #{bg} = #{body}"
end

if $PROGRAM_NAME == __FILE__
  report('#000000', '#ffffff') # 21.00
  report('#767676', '#ffffff') # 4.54
  report('nope', '#ffffff')    # invalid
end

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 →