DEV Community

Muhammad Afsar Khan
Muhammad Afsar Khan

Posted on

How I Built a WCAG Contrast Checker in 50 Lines of JavaScript.

I used to do something really dumb.

When I was picking colors for a website, I'd zoom in, squint, and ask myself: "Is this readable? Yeah, probably."

Then I'd ship it.

A few months ago, a user emailed me. He had low vision and couldn't read light gray text on one of my sites. He wasn't angry — just disappointed. Said he wanted to read my content but couldn't.

That email is still in my inbox.

So when I built FontPreview, I decided I wasn't going to guess anymore. I built a contrast checker right into the tool. Turns out it only took about 50 lines of JavaScript.

Here's exactly how it works.

The Math (It's Not as Scary as It Looks)
WCAG contrast ratio is calculated with this formula:
(L1 + 0.05) / (L2 + 0.05)
Where L1 is the relative luminance of the lighter color, and L2 is the relative luminance of the darker color.

But "relative luminance" sounds like something from a math textbook. Here's what it actually looks like in JavaScript.

javascript
function luminance(r, g, b) {
  let a = [r, g, b].map(v => {
    v /= 255;
    return v <= 0.03928 
      ? v / 12.92 
      : Math.pow((v + 0.055) / 1.055, 2.4);
  });
  return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
Enter fullscreen mode Exit fullscreen mode

I didn't invent this formula — it's straight from the WCAG spec. I just typed it into my editor and hoped it worked. (It did. Eventually.)

Converting Hex to RGB
First step: turn those hex codes (#1e1e1e, #E8E8E8) into something we can do math with.

javascript
function hexToRgb(hex) {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? {
    r: parseInt(result[1], 16),
    g: parseInt(result[2], 16),
    b: parseInt(result[3], 16)
  } : null;
}
Enter fullscreen mode Exit fullscreen mode

Regex looks scary, but it's just pulling out the red, green, and blue values from that hex string.

Putting It All Together
Once we have RGB values, we can calculate the contrast ratio between two colors:

javascript
function contrastRatio(hex1, hex2) {
  const rgb1 = hexToRgb(hex1);
  const rgb2 = hexToRgb(hex2);

  if (!rgb1 || !rgb2) return 1;

  const l1 = luminance(rgb1.r, rgb1.g, rgb1.b);
  const l2 = luminance(rgb2.r, rgb2.g, rgb2.b);

  const lighter = Math.max(l1, l2);
  const darker = Math.min(l1, l2);

  return (lighter + 0.05) / (darker + 0.05);
}
Enter fullscreen mode Exit fullscreen mode

That's it. That's the whole thing.

What's a "Good" Ratio?
WCAG defines three levels:

AAA: 7:1 or higher (the gold standard)

AA: 4.5:1 or higher (what most sites should aim for)

AA Large: 3:1 for text that's at least 24px or 19px bold

Here's how I check that in code:

javascript
function getWcagLevel(ratio, fontSize, isBold) {
  const size = parseFloat(fontSize);
  const isLarge = size >= 18.66 || (size >= 14 && isBold);

  if (ratio >= 7) return { level: 'AAA', class: 'aaa' };
  if (ratio >= 4.5) return { level: 'AA', class: 'pass' };
  if (ratio >= 3 && isLarge) return { level: 'AA Large', class: 'aa-large' };
  return { level: 'FAIL', class: 'fail' };
}
Enter fullscreen mode Exit fullscreen mode

The 18.66 and 14 numbers come from the WCAG spec — 18.66px is about 14pt at 1.333x scaling. I don't think about it too hard. I just copy the numbers and move on.

The Part That Surprised Me
I thought the math would be the hard part. It wasn't.

The hard part was realizing how many of my old designs would have failed these checks. That light gray text I thought looked "clean" and "minimal"? FAIL. That low-contrast button I was proud of? FAIL.

The email from that user makes more sense now.

You Can Try It Yourself
I built this into FontPreview. Pick any two colors, type some text, and the badge updates instantly. No math required.

If you're a developer, steal this code. Use it in your own projects. That's why I'm sharing it.

One More Thing
The 50 lines of code in this post are fine. They work. But they're not the important part.

The important part is that I stopped guessing.

If you're still checking colors by squinting at your screen, just know that someone out there — maybe someone like the guy who emailed me — is trying to read your work and can't.

Fifty lines of code is a small price to pay for making sure they can.

Try the contrast checker yourself: FontPreview

Top comments (0)