Calculate Color Contrast Ratios In JavaScript
Details
- This is a JavaScript implementation of the formula to calculate the contrast ratio between two colors
- These ratios can be used to check for potential accessibility issues. The preliminary checklist from W3C (linked below) says: Web pages should also have a minimum contrast by default: a contrast ratio of at least 4.5:1 for normal-size text.
- There are other targets of 3:1 and 7:1 depending on the circumstance which are listed in the docs.
- There are online tools for doing this calculation but I haven't seen a published JavaScript version that's just the formula for us in other code. I need one for another site I'm working on so put this together from the W3C reference
- The code blocks below are split into two sections. The first is the calculateContrastRatio() function that gets called and its supporting prepColor() function
- The second section of code is an example using the primary functions. It takes two hex values from the input fields and displays the ratio below them
- There's a bunch of duplication and not a lot of error handling. Addressing that if necessary is left as an exercise for the reader
Example
Formula
-
This is the core formula the does the work. It has two functions
`calculateContrastRatioAntecedent` and `prepColor`.
-
The `calculateContrastRatioAntecedent` function is what gets called
directly for whatever needs to get the contrast ratio. It takes two
hex values as input and returns the antecedent portion of the contrast
ratio as a float
-
The antecedent is the left number of a ratio (e.g. `4.2` in `4.2:1`)
-
The float value is all that's returned because the other poriton of
the ratio is always `1` for this methodology.
////////////////////////////////////////////////////////////////////////
// calculateContrastRatioAntecedent
////////////////////////////////////////////////////////////////////////
//
// This function the main funciton that gets called with two hex
// values. It returns the antecedent as a float. Each hex value
// is first turned into a color object with a luminocity (lum)
// property via the `prepColor()` function before being used in
// the core calculation which produces the final value to return
//
////////////////////////////////////////////////////////////////////////
const calculateContrastRatioAntecedent = (hex1, hex2) => {
const color1 = prepColor(hex1)
const color2 = prepColor(hex2)
const antecedent =
(Math.max(color1.lum, color2.lum) + 0.05) /
(Math.min(color1.lum, color2.lum) + 0.05)
return antecedent
}
////////////////////////////////////////////////////////////////////////
//prepColor
////////////////////////////////////////////////////////////////////////
//
// This funciton contains all the math for the conversion from hex
// to a color object with the luminocity value. It starts by pulling
// the individual pairs of red, green, and blue hex values out of
// the input string and then runs them through the stack of calcluations
// before combining them at the end to produce the value
//
////////////////////////////////////////////////////////////////////////
const prepColor = (hex) => {
color = {
hex: hex,
hex_r: hex.substr(1, 2),
hex_g: hex.substr(3, 2),
hex_b: hex.substr(5, 2),
}
color.rgb_r = parseInt(color.hex_r, 16)
color.rgb_g = parseInt(color.hex_g, 16)
color.rgb_b = parseInt(color.hex_b, 16)
color.tmp_r = color.rgb_r / 255
color.tmp_g = color.rgb_g / 255
color.tmp_b = color.rgb_b / 255
color.srgb_r =
color.tmp_r <= 0.03928
? color.tmp_r / 12.92
: Math.pow((color.tmp_r + 0.055) / 1.055, 2.4)
color.srgb_g =
color.tmp_g <= 0.03928
? color.tmp_g / 12.92
: Math.pow((color.tmp_g + 0.055) / 1.055, 2.4)
color.srgb_b =
color.tmp_b <= 0.03928
? color.tmp_b / 12.92
: Math.pow((color.tmp_b + 0.055) / 1.055, 2.4)
color.lum_r = 0.2126 * color.srgb_r
color.lum_g = 0.7152 * color.srgb_g
color.lum_b = 0.0722 * color.srgb_b
color.lum = color.lum_r + color.lum_g + color.lum_b
return color
}
Usage
////////////////////////////////////////////////////////////////////////
// els
////////////////////////////////////////////////////////////////////////
//
// This is a convience object that holds the individual document
// elements
//
////////////////////////////////////////////////////////////////////////
const els = {}
////////////////////////////////////////////////////////////////////////
// init
////////////////////////////////////////////////////////////////////////
//
// The core initilization function. It loads the document elements
// into the `els` object, attachest listeners to them and then fires
// off the process once to load the initial values on the page
//
////////////////////////////////////////////////////////////////////////
const init = () => {
els.in1 = document.getElementById('in1')
els.in2 = document.getElementById('in2')
els.ratio = document.getElementById('ratio')
els.in1.addEventListener('input', updateRatio)
els.in2.addEventListener('input', updateRatio)
updateRatio()
}
////////////////////////////////////////////////////////////////////////
// updateRatio
////////////////////////////////////////////////////////////////////////
//
// This function grabs the current files from the input fields
// validates that they look like hex codes and assembles and
// displays a ratio string if they do.
//
// The hex check looks for any word character so it's possible to
// send in a string of 6 characters but include ones that are
// outisde of hex (e.g. 'z') which would break. The degree to
// which that is acceptable or needs to be updates is dependent
// on implementation.
//
////////////////////////////////////////////////////////////////////////
const updateRatio = () => {
const hex1 = els.in1.value
const hex2 = els.in2.value
if (!hex1.match(/^#\w\w\w\w\w\w$/) || !hex2.match(/^#\w\w\w\w\w\w$/)) {
return null
} else {
const antecedent = calculateContrastRatioAntecedent(hex1, hex2)
const ratio = `${antecedent.toFixed(2)}:1`
els.ratio.innerHTML = ratio
}
}
// Kick everything off when the document is ready
document.addEventListener('DOMContentLoaded', init)
References