White Paper

Create Your Own Color Contrast Checker

Photo by davisco on Unsplash - Lemon painted blue on the outside sliced to reveal bright yellow within

Sufficient color contrast is a key requirement for accessible web content, and thanks to a multitude of tools for checking contrast, it’s one of the easiest accessibility criteria to meet. These tools can tell you whether a text and background color pairing meets the Web Content Accessibility Guidelines (WCAG) minimum contrast requirements, so users with limited vision can read your content.

There’s no shortage of existing options to choose from, but if you (like me) are curious how these tools calculate color contrast, you can learn a lot by coding your own. In this tutorial, we’ll step through a simple example.

Screenshot of Contrast Checker, my simple web app for checking the contrast ratio of two colors.

Color Contrast 101

Why check color contrast in the first place, let alone spend the time and energy to create a tool for it? Broadly speaking: sufficient contrast ensures everyone, including users with low vision, will be able to read the text on your website. Low contrast, on the other hand, locks out people with macular degeneration, cataracts, color blindness of many sorts, and other disabilities. The consequences of this exclusion may sound hyperbolic, but it’s hard to overstate them: If you’re working on a government website with critical public health information, low color contrast could cost lives. If you’re developing an e-commerce or marketing website, you cost your business real dollars by turning away disabled users. You owe it to yourself and your users to develop accessible content on the web.

(For more information, WebAIM provides good overviews of low vision and color blindness and how users with them navigate the web.)

Following the WCAG guidelines for color contrast are one of many steps you should take to avoid those issues. There’s a lot of research, science, and history behind the guidelines, and you can learn more in this excellent explainer by Stacie Arellano, which helped me immensely. In brief: the guidelines aim to make content readable for people with 20/40 vision, and they outline minimum contrast ratios for text to meet this standard. The absolute bare minimum (i.e. what the WCAG calls the AA level) is a 4.5:1 ratio between body text and its background, and a 3:1 ratio for large text. To meet the AAA level, text must have a 7:1 ratio with its background.

Don’t worry if you don’t know what a contrast ratio is. I had just a vague idea before I started working on this article, and it was only by turning the WCAG guidelines into code that I learned what went into a contrast ratio. I hope sharing that process can help you understand the terminology better, too. Let’s dive in!

Turning Guidelines into Code

The heart of our tool will be the WCAG formula for the relative luminance of a color. Relative luminance measures how bright a color appears to the human eye. White corresponds to a relative luminance of 1, and black to 0.

The definition of the formula begins:

For the sRGB colorspace, the relative luminance of a color is defined as:
L = 0.2126 * R + 0.7152 * G + 0.0722 * B

Pretty simple so far. How are R, G, and B defined?

if RsRGB <= 0.03928 then R = RsRGB / 12.92 else R = ((RsRGB+0.055) / 1.055) ^ 2.4

if GsRGB <= 0.03928 then G = GsRGB / 12.92 else G = ((GsRGB+0.055) / 1.055) ^ 2.4

if BsRGB <= 0.03928 then B = BsRGB / 12.92 else B = ((BsRGB+0.055) / 1.055) ^ 2.4

That’s…a little more complicated, but we’re almost done. Here’s the last part of the formula definition:

RsRGB = R8bit/255

GsRGB = G8bit/255

BsRGB = B8bit/255

There’s our starting point. R8bit, G8bit, and B8bit are just numbers, ranging from 0 to 255, that represent the red, green, and blue components of a color. (If you’ve ever seen a value like rgb(134, 23, 251) in CSS, those are the numbers we’re talking about.)

Now that we know our inputs, we can start to turn this formula into JavaScript. Our goal is to write a function that takes three numbers — the red, green, and blue components of a color — and returns the relative luminance value. Create a file called contrast.js and open it in your editor. Let’s call our function luminance:

function luminance(r, g, b) {
    // Implement formula here...

With formulas like this, it’s often helpful to start from the bottom up. If we start at the end of the formula definition, we see that we should start by dividing each of R8bit, G8bit, and B8bit by 255.

function luminance(r, g, b) {
    [r, g, b].map(component => {
        let proportion = component / 255;

        // TODO: Return the proportion
    // TODO: The rest of the formula

Next, following the middle portion of the formula definition, we check if each proportion is less than or equal to 0.03928, and transform it accordingly.

function luminance(r, g, b) {
    [r, g, b].map(component => {
        let proportion = component / 255;

        return proportion <= 0.03928
            ? proportion / 12.92
            : Math.pow((proportion + 0.055) / 1.055, 2.4);

    // TODO: The rest of the formula

Finally, we combine the resulting values and return the result.

function luminance(r, g, b) {
    let [lumR, lumG, lumB] = [r, g, b].map(component => {
        let proportion = component / 255;

        return proportion <= 0.03928
            ? proportion / 12.92
            : Math.pow((proportion + 0.055) / 1.055, 2.4);

    return 0.2126 * lumR + 0.7152 * lumG + 0.0722 * lumB;

That’s it! You can test the function with the inputs for white (255, 255, 255) and black (0, 0, 0) to see it working.

console.log(luminance(255, 255, 255))
// Output: 1
console.log(luminance(0, 0, 0))
// Output: 0

Converting luminance values to a contrast ratio is straightforward.

The WCAG definition of contrast ratio is:

(L1 + 0.05) / (L2 + 0.05), where

L1 is the relative luminance of the lighter of the colors, and

L2 is the relative luminance of the darker of the colors.

We can add this to contrast.js as another function — we’ll call it contrastRatio.

function contrastRatio(luminance1, luminance2) {
    let lighterLum = Math.max(luminance1, luminance2);
    let darkerLum = Math.min(luminance1, luminance2);

    return (lighterLum + 0.05) / (darkerLum + 0.05);

Now that we’ve implemented the contrast guidelines in our script, let’s turn our attention to the app itself.

Setting up the app

In the same directory as contrast.js, create two new files: index.html and styles.css. For the sake of brevity, I won’t include their contents here, but you can find them on GitHub. index.html provides the basic HTML skeleton of our app, including some preview text, a status indicator, and the inputs for changing our foreground and background colors. styles.css sets up some basic styles — it’ll make your app look like the screenshot I included above, but you’re of course free to modify the styles to your liking. 🙂

That’s all we need to set up a static version of our contrast checker. You can open index.html in your browser to see how it looks, but of course, our app isn’t functional yet. Now we need to connect our HTML to the functions we wrote in contrast.js.

Bringing the contrast checker to life

In contrast.js, let’s start by grabbing references to the two color inputs and all the elements we’ll need to update:

let preview = document.getElementById('preview')
let statusText = document.getElementById('status-text')
let statusRatio = document.getElementById('status-ratio')
let statusLevel = document.getElementById('status-level')
let textColorInput = document.getElementById('input-text')
let bgColorInput = document.getElementById('input-background')

We’ll need to update the UI whenever either color input changes. Let’s add an event listener to each called handleColorChange:

textColorInput.addEventListener('input', handleColorChange)
bgColorInput.addEventListener('input', handleColorChange)

(The input event fires whenever the user changes the input value — as opposed to the confusingly named change event, which only fires when the input loses focus. Listening on input allows us to update the preview and status bar in real time.)

Next, let’s define handleColorChange:

function handleColorChange() {
    // Grab the latest value from each input
    let textColor = textColorInput.value
    let bgColor = bgColorInput.value

    // Update the preview to use the latest values
    preview.style.color = textColor
    preview.style.backgroundColor = bgColor

    let ratio = checkContrast(textColor, bgColor)
    let { didPass, maxLevel } = meetsMinimumRequirements(ratio)

    // Update the status bar with the new ratio, whether it passes
    // or fails, and level passed
    statusText.classList.toggle('is-pass', didPass)
    statusRatio.innerText = formatRatio(ratio)
    statusLevel.innerText = didPass ? maxLevel : 'Fail'

You’ll notice that handleColorChange references a few functions we haven’t defined yet. Let’s add those now.

 * Because color inputs format their values as hex strings (ex.
 * #000000), we have to do a little parsing to extract the red,
 * green, and blue components as numbers before calculating the 
 * luminance values and contrast ratio.
function checkContrast(color1, color2) {
    let [luminance1, luminance2] = [color1, color2].map(color => {
        /* Remove the leading hash sign if it exists */
        color = color.startsWith("#") ? color.slice(1) : color;

        let r = parseInt(color.slice(0, 2), 16);
        let g = parseInt(color.slice(2, 4), 16);
        let b = parseInt(color.slice(4, 6), 16);

        return luminance(r, g, b);

    return contrastRatio(luminance1, luminance2);

 * A utility to format ratios as nice, human-readable strings with 
 * up to two digits after the decimal point (ex. "4.3:1" or "17:1")
function formatRatio(ratio) {
    let ratioAsFloat = ratio.toFixed(2)
    let isInteger = Number.isInteger(parseFloat(ratioAsFloat))
    return `${isInteger ? Math.floor(ratio) : ratioAsFloat}:1`

 * Determine whether the given contrast ratio meets WCAG 
 * requirements at any level (AA Large, AA, or AAA). In the return 
 * value, `isPass` is true if the ratio meets or exceeds the minimum 
 * of at least one level, and `maxLevel` is the strictest level that 
 * the ratio passes.
    ['AA Large', 3],
    ['AA', 4.5],
    ['AAA', 7],

function meetsMinimumRequirements(ratio) {
    let didPass = false
    let maxLevel = null

    for (const [level, minRatio] of WCAG_MINIMUM_RATIOS) {
        if (ratio < minRatio) break

        didPass = true
        maxLevel = level

    return { didPass, maxLevel }

The final step is to add a manual call to handleColorChange to initialize the app:


That’s it! You should now be able to open index.html in your browser and test out color pairings to your heart’s content.


This was a simple example, and there are plenty of better tools that already exist to check color contrast. But the exercise of creating your own tool is valuable nonetheless. For me, figuring out how to code my own contrast checker demystified those off-the-shelf tools, and it gave me a chance to slow down and really understand the WCAG standards we’re aiming for.

The WCAG documentation can often be dense and hard to comprehend. Coding your way through a complex guideline can be one of your best strategies for deepening your accessibility know-how. Make more simple tools for yourself. Even if you don’t end up using the end product, the learning along the way will be invaluable.

Up Next


The ADA and your website

Applying the principles of the Americans With Disabilities Act to your website is more important than ever. Not just because it’s the right right thing to do and will actually benefit all users, but because it makes good business sense.

Never Miss an Insight

Sign up for emails from Tamman

This field is for validation purposes and should be left unchanged.