Give it a try

  1. To see your bookmarks bar, press the keys (Ctrl/Cmd + Shift + B) .
  2. Drag the "SpotCheck - Contrast Checker" link to your bookmarks bar or bookmark folder.
  3. Open any page, then click the bookmark to run it.
  4. Click it again or click the close button in the popup to clear the results.

Example of the popup that will appear in the bottom right corner of your browser with a summary of the results.

Contrast Checker 26 AAA   25 AA only   1 fail

What the colors mean

Each text block is outlined and labeled with its measured contrast ratio.

Correct

Contrast ratio of at least 7:1 (4.5:1 for large text), which passes the enhanced AAA level.

Flagged

Passes the AA minimum but falls short of AAA: legible, but not the most robust choice.

Incorrect

Falls below the WCAG AA minimum of 4.5:1 (3:1 for large text), likely hard to read for people with low vision.

Try it here

Run the check against the sample markup below without leaving this page.

Low contrast text (fails AA)

Borderline text (passes AA, not AAA)

High contrast text (passes AAA)

Applicable WCAG 2.2 success criteria

Sufficient contrast between text and its background is one of the most common accessibility failures, and one of the easiest to test for automatically.

  • 1.4.3
    Contrast (Minimum)(Level AA)

    Text needs a contrast ratio of at least 4.5:1 against its background, or 3:1 for large-scale text.

  • 1.4.6
    Contrast (Enhanced)(Level AAA)

    The strictest level raises the bar to 7:1 for normal text and 4.5:1 for large text.

Read the source

The bookmarklet contains the TL;DR version of the code. Here's the readable version.

(function () {
  var NS = '__a11yContrastChecker';
  if (window[NS] && window[NS].active) {
    window[NS].teardown();
    return;
  }

  function parseColor(str) {
    var m = str.match(/rgba?\(([^)]+)\)/);
    if (!m) return null;
    var parts = m[1].split(',').map(function (s) { return parseFloat(s); });
    return { r: parts[0], g: parts[1], b: parts[2], a: parts.length > 3 ? parts[3] : 1 };
  }

  function relLuminance(c) {
    function chan(v) {
      v = v / 255;
      return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
    }
    return 0.2126 * chan(c.r) + 0.7152 * chan(c.g) + 0.0722 * chan(c.b);
  }

  function contrastRatio(c1, c2) {
    var l1 = relLuminance(c1) + 0.05;
    var l2 = relLuminance(c2) + 0.05;
    return l1 > l2 ? l1 / l2 : l2 / l1;
  }

  function getEffectiveBackground(el) {
    var node = el;
    while (node) {
      var style = getComputedStyle(node);
      var bg = parseColor(style.backgroundColor);
      if (bg && bg.a > 0) return bg;
      node = node.parentElement;
    }
    return { r: 255, g: 255, b: 255, a: 1 };
  }

  function isLargeText(style) {
    var size = parseFloat(style.fontSize);
    var weight = parseInt(style.fontWeight, 10) || 400;
    return size >= 24 || (size >= 18.66 && weight >= 700);
  }

  var textEls = Array.prototype.filter.call(document.querySelectorAll('body *'), function (el) {
    if (!el.childNodes.length) return false;
    var hasOwnText = Array.prototype.some.call(el.childNodes, function (n) {
      return n.nodeType === 3 && n.textContent.trim().length > 0;
    });
    if (!hasOwnText) return false;
    var rect = el.getBoundingClientRect();
    return rect.width > 0 && rect.height > 0;
  });

  var records = [];
  var counts = { green: 0, gold: 0, red: 0 };

  function mark(el, level, label) {
    records.push({ el: el, outline: el.style.outline, outlineOffset: el.style.outlineOffset, position: el.style.position, badge: null });
    var color = level === 'green' ? '#1a7d4f' : level === 'gold' ? '#8b6800' : '#be412a';
    el.style.outline = '3px solid ' + color;
    el.style.outlineOffset = '1px';
    if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
    var badge = document.createElement('span');
    badge.textContent = label;
    badge.setAttribute('aria-hidden', 'true');
    badge.style.cssText = [
      'position:absolute', 'top:0', 'left:0', 'transform:translateY(-100%)',
      'background:' + color, 'color:#fff', 'font:600 14px monospace',
      'padding:1px 4px', 'border-radius:3px 3px 0 0', 'z-index:2147483000',
      'pointer-events:none', 'white-space:nowrap'
    ].join(';');
    el.insertBefore(badge, el.firstChild);
    records[records.length - 1].badge = badge;
    counts[level]++;
  }

  textEls.forEach(function (el) {
    var style = getComputedStyle(el);
    var fg = parseColor(style.color);
    if (!fg) return;
    var bg = getEffectiveBackground(el);
    var ratio = contrastRatio(fg, bg);
    var large = isLargeText(style);
    var aaMin = large ? 3 : 4.5;
    var aaaMin = large ? 4.5 : 7;
    var label = ratio.toFixed(2) + ':1';

    if (ratio < aaMin) {
      mark(el, 'red', label + ', fails AA');
    } else if (ratio < aaaMin) {
      mark(el, 'gold', label + ', passes AA, not AAA');
    } else {
      mark(el, 'green', label + ', passes AAA');
    }
  });

  var panel = document.createElement('div');
  panel.setAttribute('data-a11y-bm-panel', 'contrast');
  panel.setAttribute('role', 'status');
  panel.setAttribute('aria-live', 'polite');
  panel.style.cssText = [
    'position:fixed', 'bottom:16px', 'right:16px', 'z-index:2147483001',
    'background:#1b2430', 'color:#e7eaed', 'font:13px/1.5 -apple-system,sans-serif',
    'padding:12px 32px 12px 16px', 'border-radius:6px', 'box-shadow:0 4px 16px rgba(0,0,0,.3)',
    'max-width:260px'
  ].join(';');
  panel.innerHTML =
    '<strong style="display:block;margin-bottom:4px;">Text Contrast Checker</strong>' +
    '<span style="color:#3fbf7f;">&#9679;</span> ' + counts.green + ' AAA &nbsp; ' +
    '<span style="color:#e0b03f;">&#9679;</span> ' + counts.gold + ' AA only &nbsp; ' +
    '<span style="color:#e06a4f;">&#9679;</span> ' + counts.red + ' fail';
  var closeBtn = document.createElement('button');
  closeBtn.type = 'button';
  closeBtn.textContent = '\u00d7';
  closeBtn.setAttribute('aria-label', 'Close');
  closeBtn.style.cssText = [
    'position:absolute', 'top:4px', 'right:6px',
    'background:transparent', 'border:none', 'color:#e7eaed',
    'font:16px/1 -apple-system,sans-serif', 'cursor:pointer',
    'padding:2px 6px', 'border-radius:4px', 'line-height:1'
  ].join(';');
  closeBtn.addEventListener('click', function () { window[NS].teardown(); });
  panel.appendChild(closeBtn);
  document.body.appendChild(panel);

  function teardown() {
    records.forEach(function (r) {
      r.el.style.outline = r.outline;
      r.el.style.outlineOffset = r.outlineOffset;
      r.el.style.position = r.position;
      if (r.badge && r.badge.parentNode) r.badge.parentNode.removeChild(r.badge);
    });
    if (panel.parentNode) panel.parentNode.removeChild(panel);
  }

  window[NS] = {
    active: true,
    teardown: function () {
      teardown();
      window[NS].active = false;
    }
  };
})();

FYI

  • Large text is defined as 24px and up, or 18.66px and up when bold; the minimum ratio required drops from 4.5:1 to 3:1 at that size.
  • Background color is resolved by walking up the DOM until a non-transparent background is found, defaulting to white if none is set.
  • This check covers text contrast only. 1.4.11 Non-text Contrast for icons and UI controls is a separate manual check.