Free tool

Contrast Bookmarklet

Drag the button below to your bookmarks bar. Click it on any page to flag every text element that fails WCAG AA — runs in your browser, no server, no SSRF risk, works on intranet and auth-walled sites.

Runs in your browser — your URLs and content stay on your device

Step 1 — show your bookmarks bar (if hidden)

  • Chrome / Edge: Cmd-Shift-B (Mac) or Ctrl-Shift-B (Windows).
  • Firefox: View menu → Toolbars → Bookmarks Toolbar.
  • Safari: View menu → Show Favourites Bar (or Cmd-Shift-B).

Step 2 — install the bookmarklet (try drag first, fall back to manual)

Drag the button onto your bookmarks bar. Don\'t click it on this page.

Drag dropped you on a Google search? Install manually instead.

Chrome and Edge sometimes refuse to bookmark a javascript: URL via drag — the dropped text ends up in the address bar and runs as a search query. (The link itself is fine; the browser is just being cautious.) The manual path below always works.

javascript:(function%20()%20%7B%20//%20Toggle%20off%20if%20already%20running%20var%20oldPanel%20=%20document.getElementById('lb-contrast-panel');%20if%20(oldPanel)%20%7B%20oldPanel.remove();%20document.querySelectorAll('%5Bdata-lb-contrast%5D').forEach(function%20(el)%20%7B%20el.style.outline%20=%20el.dataset.lbOutlineOriginal%20%7C%7C%20'';%20el.style.outlineOffset%20=%20el.dataset.lbOffsetOriginal%20%7C%7C%20'';%20el.removeAttribute('data-lb-contrast');%20%7D);%20return;%20%7D%20function%20parseRgb(s)%20%7B%20var%20m%20=%20s%20%26%26%20s.match(/rgba?%5C((%5B%5Cd.%5D+),%5Cs*(%5B%5Cd.%5D+),%5Cs*(%5B%5Cd.%5D+)(?:,%5Cs*(%5B%5Cd.%5D+))?%5C)/);%20if%20(!m)%20return%20null;%20return%20%5B+m%5B1%5D,%20+m%5B2%5D,%20+m%5B3%5D,%20m%5B4%5D%20?%20+m%5B4%5D%20:%201%5D;%20%7D%20function%20blend(fg,%20bg)%20%7B%20var%20a%20=%20fg%5B3%5D;%20return%20%5Bfg%5B0%5D*a%20+%20bg%5B0%5D*(1-a),%20fg%5B1%5D*a%20+%20bg%5B1%5D*(1-a),%20fg%5B2%5D*a%20+%20bg%5B2%5D*(1-a),%201%5D;%20%7D%20function%20relLum(rgb)%20%7B%20var%20c%20=%20rgb.slice(0,%203).map(function%20(v)%20%7B%20v%20/=%20255;%20return%20v%20%3C=%200.03928%20?%20v%20/%2012.92%20:%20Math.pow((v%20+%200.055)%20/%201.055,%202.4);%20%7D);%20return%200.2126%20*%20c%5B0%5D%20+%200.7152%20*%20c%5B1%5D%20+%200.0722%20*%20c%5B2%5D;%20%7D%20function%20ratio(a,%20b)%20%7B%20var%20l1%20=%20relLum(a),%20l2%20=%20relLum(b);%20return%20(Math.max(l1,%20l2)%20+%200.05)%20/%20(Math.min(l1,%20l2)%20+%200.05);%20%7D%20function%20effectiveBg(el)%20%7B%20var%20hasImage%20=%20false;%20var%20bg%20=%20%5B255,%20255,%20255,%201%5D;%20var%20node%20=%20el;%20var%20stacked%20=%20%5B%5D;%20while%20(node%20%26%26%20node%20!==%20document.documentElement)%20%7B%20var%20cs%20=%20getComputedStyle(node);%20if%20(cs.backgroundImage%20%26%26%20cs.backgroundImage%20!==%20'none')%20hasImage%20=%20true;%20var%20c%20=%20parseRgb(cs.backgroundColor);%20if%20(c%20%26%26%20c%5B3%5D%20%3E%200)%20%7B%20if%20(c%5B3%5D%20===%201)%20%7B%20//%20Opaque%20ancestor%20%E2%80%94%20apply%20any%20partials%20we%20collected%20on%20top%20var%20resolved%20=%20c;%20for%20(var%20i%20=%20stacked.length%20-%201;%20i%20%3E=%200;%20i--)%20resolved%20=%20blend(stacked%5Bi%5D,%20resolved);%20return%20%7B%20color:%20resolved,%20hasImage:%20hasImage%20%7D;%20%7D%20stacked.push(c);%20%7D%20node%20=%20node.parentElement;%20%7D%20//%20No%20opaque%20ancestor%20%E2%80%94%20assume%20page%20bg%20is%20white%20var%20resolved2%20=%20bg;%20for%20(var%20j%20=%20stacked.length%20-%201;%20j%20%3E=%200;%20j--)%20resolved2%20=%20blend(stacked%5Bj%5D,%20resolved2);%20return%20%7B%20color:%20resolved2,%20hasImage:%20hasImage%20%7D;%20%7D%20function%20isLargeText(cs)%20%7B%20var%20sz%20=%20parseFloat(cs.fontSize);%20var%20weight%20=%20parseInt(cs.fontWeight,%2010)%20%7C%7C%20400;%20if%20(sz%20%3E=%2024)%20return%20true;%20//%20~18pt%20regular%20if%20(sz%20%3E=%2018.66%20%26%26%20weight%20%3E=%20700)%20return%20true;%20//%20~14pt%20bold%20return%20false;%20%7D%20//%20Walk%20every%20element%20that%20directly%20contains%20text%20var%20fails%20=%20%5B%5D;%20var%20nodes%20=%20document.querySelectorAll('body%20*');%20for%20(var%20i%20=%200;%20i%20%3C%20nodes.length;%20i++)%20%7B%20var%20el%20=%20nodes%5Bi%5D;%20if%20(el.closest('script,style,noscript'))%20continue;%20var%20hasDirectText%20=%20false;%20for%20(var%20n%20=%20el.firstChild;%20n;%20n%20=%20n.nextSibling)%20%7B%20if%20(n.nodeType%20===%203%20%26%26%20n.nodeValue.trim())%20%7B%20hasDirectText%20=%20true;%20break;%20%7D%20%7D%20if%20(!hasDirectText)%20continue;%20var%20cs%20=%20getComputedStyle(el);%20if%20(cs.visibility%20===%20'hidden'%20%7C%7C%20cs.display%20===%20'none')%20continue;%20var%20fg%20=%20parseRgb(cs.color);%20if%20(!fg%20%7C%7C%20fg%5B3%5D%20===%200)%20continue;%20var%20bg%20=%20effectiveBg(el);%20var%20fgFlat%20=%20fg%5B3%5D%20===%201%20?%20fg%20:%20blend(fg,%20bg.color);%20var%20r%20=%20ratio(fgFlat,%20bg.color);%20var%20threshold%20=%20isLargeText(cs)%20?%203%20:%204.5;%20if%20(r%20%3C%20threshold)%20%7B%20fails.push(%7B%20el:%20el,%20ratio:%20r,%20threshold:%20threshold,%20text:%20(el.innerText%20%7C%7C%20'').slice(0,%2080),%20hasImage:%20bg.hasImage,%20large:%20threshold%20===%203%20%7D);%20el.dataset.lbOutlineOriginal%20=%20el.style.outline%20%7C%7C%20'';%20el.dataset.lbOffsetOriginal%20=%20el.style.outlineOffset%20%7C%7C%20'';%20el.style.outline%20=%20'2px%20solid%20%23ef4444';%20el.style.outlineOffset%20=%20'2px';%20el.setAttribute('data-lb-contrast',%20'1');%20%7D%20%7D%20//%20Build%20the%20floating%20panel%20var%20panel%20=%20document.createElement('div');%20panel.id%20=%20'lb-contrast-panel';%20Object.assign(panel.style,%20%7B%20position:%20'fixed',%20top:%20'16px',%20right:%20'16px',%20width:%20'360px',%20maxHeight:%20'80vh',%20display:%20'flex',%20flexDirection:%20'column',%20background:%20'%230c111a',%20color:%20'%23e5e7eb',%20borderRadius:%20'12px',%20boxShadow:%20'0%2010px%2030px%20rgba(0,0,0,0.4)',%20zIndex:%20'2147483647',%20fontFamily:%20'system-ui,-apple-system,sans-serif',%20fontSize:%20'13px',%20border:%20'1px%20solid%20%231f2937',%20overflow:%20'hidden'%20%7D);%20var%20header%20=%20document.createElement('div');%20Object.assign(header.style,%20%7B%20padding:%20'14px%2016px',%20borderBottom:%20'1px%20solid%20%231f2937',%20display:%20'flex',%20justifyContent:%20'space-between',%20alignItems:%20'baseline',%20gap:%20'12px'%20%7D);%20header.innerHTML%20=%20'%3Cdiv%3E%3Cstrong%20style=%22display:block;font-size:14px;color:white%22%3EContrast%20scan%3C/strong%3E'%20+%20'%3Cspan%20style=%22opacity:0.6;font-size:11px%22%3E'%20+%20fails.length%20+%20'%20issue'%20+%20(fails.length%20===%201%20?%20''%20:%20's')%20+%20'%20below%20WCAG%20AA%3C/span%3E%3C/div%3E'%20+%20'%3Cbutton%20id=%22lb-contrast-close%22%20aria-label=%22Close%22%20'%20+%20'style=%22background:none;border:0;color:%239ca3af;cursor:pointer;font-size:20px;line-height:1;padding:0%22%3E%C3%97%3C/button%3E';%20panel.appendChild(header);%20var%20body%20=%20document.createElement('div');%20Object.assign(body.style,%20%7B%20padding:%20'8px%200',%20overflow:%20'auto',%20flex:%20'1'%20%7D);%20if%20(fails.length%20===%200)%20%7B%20body.innerHTML%20=%20'%3Cp%20style=%22padding:24px%2016px;text-align:center;color:%239ca3af;line-height:1.5%22%3E'%20+%20'No%20AA%20contrast%20issues%20found.%3Cbr%3E'%20+%20%22That%20doesn't%20mean%20the%20page%20is%20fully%20accessible%20%E2%80%94%20try%20a%20screen%20reader%20and%20keyboard%20nav%20too.%3C/p%3E%22;%20%7D%20else%20%7B%20fails.forEach(function%20(f,%20idx)%20%7B%20var%20item%20=%20document.createElement('button');%20Object.assign(item.style,%20%7B%20display:%20'block',%20width:%20'100%25',%20textAlign:%20'left',%20padding:%20'10px%2016px',%20border:%20'0',%20background:%20'transparent',%20color:%20'inherit',%20cursor:%20'pointer',%20fontSize:%20'12.5px',%20borderBottom:%20idx%20%3C%20fails.length%20-%201%20?%20'1px%20solid%20%231f2937'%20:%20'none'%20%7D);%20item.onmouseenter%20=%20function%20()%20%7B%20item.style.background%20=%20'%23111827';%20%7D;%20item.onmouseleave%20=%20function%20()%20%7B%20item.style.background%20=%20'transparent';%20%7D;%20item.onclick%20=%20function%20()%20%7B%20f.el.scrollIntoView(%7B%20behavior:%20'smooth',%20block:%20'center'%20%7D);%20f.el.style.outline%20=%20'4px%20solid%20%23ef4444';%20setTimeout(function%20()%20%7B%20f.el.style.outline%20=%20'2px%20solid%20%23ef4444';%20%7D,%20800);%20%7D;%20var%20ratioColor%20=%20f.ratio%20%3C%202%20?%20'%23ef4444'%20:%20f.ratio%20%3C%203%20?%20'%23f59e0b'%20:%20'%23fbbf24';%20var%20label%20=%20f.large%20?%20'Large%20text%20%C2%B7%20%E2%89%A53:1'%20:%20'Body%20text%20%C2%B7%20%E2%89%A54.5:1';%20var%20note%20=%20f.hasImage%20?%20'%3Cbr%3E%3Cspan%20style=%22opacity:0.5%22%3E%E2%9A%A0%20Background%20image%20%E2%80%94%20score%20may%20be%20approximate%3C/span%3E'%20:%20'';%20item.innerHTML%20=%20'%3Cdiv%20style=%22display:flex;justify-content:space-between;gap:8px;margin-bottom:4px%22%3E'%20+%20'%3Cspan%20style=%22opacity:0.6%22%3E'%20+%20label%20+%20'%3C/span%3E'%20+%20'%3Cstrong%20style=%22color:'%20+%20ratioColor%20+%20';font-variant-numeric:tabular-nums%22%3E'%20+%20f.ratio.toFixed(2)%20+%20':1%3C/strong%3E%3C/div%3E'%20+%20'%3Cdiv%20style=%22white-space:nowrap;overflow:hidden;text-overflow:ellipsis%22%3E'%20+%20(f.text%20%7C%7C%20'%3Cem%3E(empty)%3C/em%3E')%20+%20'%3C/div%3E'%20+%20note;%20body.appendChild(item);%20%7D);%20%7D%20panel.appendChild(body);%20var%20footer%20=%20document.createElement('div');%20Object.assign(footer.style,%20%7B%20padding:%20'10px%2016px',%20borderTop:%20'1px%20solid%20%231f2937',%20fontSize:%20'11px',%20opacity:%20'0.5'%20%7D);%20footer.textContent%20=%20'LearnBuilder%20contrast%20bookmarklet%20%E2%80%94%20click%20an%20issue%20to%20jump%20to%20it';%20panel.appendChild(footer);%20document.body.appendChild(panel);%20document.getElementById('lb-contrast-close').onclick%20=%20function%20()%20%7B%20panel.remove();%20document.querySelectorAll('%5Bdata-lb-contrast%5D').forEach(function%20(el)%20%7B%20el.style.outline%20=%20el.dataset.lbOutlineOriginal%20%7C%7C%20'';%20el.style.outlineOffset%20=%20el.dataset.lbOffsetOriginal%20%7C%7C%20'';%20el.removeAttribute('data-lb-contrast');%20%7D);%20%7D;%20%7D)();
  1. Chrome / Edge: right-click the bookmarks bar → Add page…. Paste the URL above into the URL field, type any name (e.g. "Check Contrast"), save.
  2. Firefox: right-click the bookmarks toolbar → New Bookmark…. Paste the URL into Location, type a name, save.
  3. Safari: Bookmarks menu → Add Bookmark… → save to Favourites. Then open the bookmark in Edit Bookmarks and replace the URL with the one above.

Some enterprise-managed browsers block javascript: bookmarks entirely. If neither path works, your IT policy is preventing it — you'll need to use a regular browser profile or an extension like axe DevTools instead.

Step 3 — use it on any page

  1. Open the page you want to check (your course, an LMS preview, anything).
  2. Click the Check Contrast bookmark.
  3. A panel appears in the top-right. Failing elements are outlined in red. Click any item in the list to scroll to it.
  4. Click the bookmark again — or the × — to clear all the highlights.

View the source

Read the bookmarklet's source before you trust it on real pages. We don't track usage, ping a server, or send anything anywhere — but you don't have to take our word for it.

(function () {
  // Toggle off if already running
  var oldPanel = document.getElementById('lb-contrast-panel');
  if (oldPanel) {
    oldPanel.remove();
    document.querySelectorAll('[data-lb-contrast]').forEach(function (el) {
      el.style.outline = el.dataset.lbOutlineOriginal || '';
      el.style.outlineOffset = el.dataset.lbOffsetOriginal || '';
      el.removeAttribute('data-lb-contrast');
    });
    return;
  }

  function parseRgb(s) {
    var m = s && s.match(/rgba?\(([\d.]+),\s*([\d.]+),\s*([\d.]+)(?:,\s*([\d.]+))?\)/);
    if (!m) return null;
    return [+m[1], +m[2], +m[3], m[4] ? +m[4] : 1];
  }

  function blend(fg, bg) {
    var a = fg[3];
    return [fg[0]*a + bg[0]*(1-a), fg[1]*a + bg[1]*(1-a), fg[2]*a + bg[2]*(1-a), 1];
  }

  function relLum(rgb) {
    var c = rgb.slice(0, 3).map(function (v) {
      v /= 255;
      return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
    });
    return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
  }

  function ratio(a, b) {
    var l1 = relLum(a), l2 = relLum(b);
    return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
  }

  function effectiveBg(el) {
    var hasImage = false;
    var bg = [255, 255, 255, 1];
    var node = el;
    var stacked = [];
    while (node && node !== document.documentElement) {
      var cs = getComputedStyle(node);
      if (cs.backgroundImage && cs.backgroundImage !== 'none') hasImage = true;
      var c = parseRgb(cs.backgroundColor);
      if (c && c[3] > 0) {
        if (c[3] === 1) {
          // Opaque ancestor — apply any partials we collected on top
          var resolved = c;
          for (var i = stacked.length - 1; i >= 0; i--) resolved = blend(stacked[i], resolved);
          return { color: resolved, hasImage: hasImage };
        }
        stacked.push(c);
      }
      node = node.parentElement;
    }
    // No opaque ancestor — assume page bg is white
    var resolved2 = bg;
    for (var j = stacked.length - 1; j >= 0; j--) resolved2 = blend(stacked[j], resolved2);
    return { color: resolved2, hasImage: hasImage };
  }

  function isLargeText(cs) {
    var sz = parseFloat(cs.fontSize);
    var weight = parseInt(cs.fontWeight, 10) || 400;
    if (sz >= 24) return true;            // ~18pt regular
    if (sz >= 18.66 && weight >= 700) return true; // ~14pt bold
    return false;
  }

  // Walk every element that directly contains text
  var fails = [];
  var nodes = document.querySelectorAll('body *');
  for (var i = 0; i < nodes.length; i++) {
    var el = nodes[i];
    if (el.closest('script,style,noscript')) continue;
    var hasDirectText = false;
    for (var n = el.firstChild; n; n = n.nextSibling) {
      if (n.nodeType === 3 && n.nodeValue.trim()) { hasDirectText = true; break; }
    }
    if (!hasDirectText) continue;

    var cs = getComputedStyle(el);
    if (cs.visibility === 'hidden' || cs.display === 'none') continue;
    var fg = parseRgb(cs.color);
    if (!fg || fg[3] === 0) continue;

    var bg = effectiveBg(el);
    var fgFlat = fg[3] === 1 ? fg : blend(fg, bg.color);
    var r = ratio(fgFlat, bg.color);
    var threshold = isLargeText(cs) ? 3 : 4.5;

    if (r < threshold) {
      fails.push({
        el: el, ratio: r, threshold: threshold,
        text: (el.innerText || '').slice(0, 80),
        hasImage: bg.hasImage, large: threshold === 3
      });
      el.dataset.lbOutlineOriginal = el.style.outline || '';
      el.dataset.lbOffsetOriginal = el.style.outlineOffset || '';
      el.style.outline = '2px solid #ef4444';
      el.style.outlineOffset = '2px';
      el.setAttribute('data-lb-contrast', '1');
    }
  }

  // Build the floating panel
  var panel = document.createElement('div');
  panel.id = 'lb-contrast-panel';
  Object.assign(panel.style, {
    position: 'fixed', top: '16px', right: '16px', width: '360px',
    maxHeight: '80vh', display: 'flex', flexDirection: 'column',
    background: '#0c111a', color: '#e5e7eb', borderRadius: '12px',
    boxShadow: '0 10px 30px rgba(0,0,0,0.4)', zIndex: '2147483647',
    fontFamily: 'system-ui,-apple-system,sans-serif', fontSize: '13px',
    border: '1px solid #1f2937', overflow: 'hidden'
  });

  var header = document.createElement('div');
  Object.assign(header.style, {
    padding: '14px 16px', borderBottom: '1px solid #1f2937',
    display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '12px'
  });
  header.innerHTML = '<div><strong style="display:block;font-size:14px;color:white">Contrast scan</strong>'
    + '<span style="opacity:0.6;font-size:11px">' + fails.length + ' issue'
    + (fails.length === 1 ? '' : 's')
    + ' below WCAG AA</span></div>'
    + '<button id="lb-contrast-close" aria-label="Close" '
    + 'style="background:none;border:0;color:#9ca3af;cursor:pointer;font-size:20px;line-height:1;padding:0">×</button>';
  panel.appendChild(header);

  var body = document.createElement('div');
  Object.assign(body.style, { padding: '8px 0', overflow: 'auto', flex: '1' });

  if (fails.length === 0) {
    body.innerHTML = '<p style="padding:24px 16px;text-align:center;color:#9ca3af;line-height:1.5">'
      + 'No AA contrast issues found.<br>'
      + "That doesn't mean the page is fully accessible — try a screen reader and keyboard nav too.</p>";
  } else {
    fails.forEach(function (f, idx) {
      var item = document.createElement('button');
      Object.assign(item.style, {
        display: 'block', width: '100%', textAlign: 'left',
        padding: '10px 16px', border: '0', background: 'transparent', color: 'inherit',
        cursor: 'pointer', fontSize: '12.5px',
        borderBottom: idx < fails.length - 1 ? '1px solid #1f2937' : 'none'
      });
      item.onmouseenter = function () { item.style.background = '#111827'; };
      item.onmouseleave = function () { item.style.background = 'transparent'; };
      item.onclick = function () {
        f.el.scrollIntoView({ behavior: 'smooth', block: 'center' });
        f.el.style.outline = '4px solid #ef4444';
        setTimeout(function () { f.el.style.outline = '2px solid #ef4444'; }, 800);
      };
      var ratioColor = f.ratio < 2 ? '#ef4444' : f.ratio < 3 ? '#f59e0b' : '#fbbf24';
      var label = f.large ? 'Large text · ≥3:1' : 'Body text · ≥4.5:1';
      var note = f.hasImage
        ? '<br><span style="opacity:0.5">⚠ Background image — score may be approximate</span>'
        : '';
      item.innerHTML = '<div style="display:flex;justify-content:space-between;gap:8px;margin-bottom:4px">'
        + '<span style="opacity:0.6">' + label + '</span>'
        + '<strong style="color:' + ratioColor + ';font-variant-numeric:tabular-nums">'
        + f.ratio.toFixed(2) + ':1</strong></div>'
        + '<div style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'
        + (f.text || '<em>(empty)</em>') + '</div>' + note;
      body.appendChild(item);
    });
  }
  panel.appendChild(body);

  var footer = document.createElement('div');
  Object.assign(footer.style, {
    padding: '10px 16px', borderTop: '1px solid #1f2937', fontSize: '11px', opacity: '0.5'
  });
  footer.textContent = 'LearnBuilder contrast bookmarklet — click an issue to jump to it';
  panel.appendChild(footer);

  document.body.appendChild(panel);
  document.getElementById('lb-contrast-close').onclick = function () {
    panel.remove();
    document.querySelectorAll('[data-lb-contrast]').forEach(function (el) {
      el.style.outline = el.dataset.lbOutlineOriginal || '';
      el.style.outlineOffset = el.dataset.lbOffsetOriginal || '';
      el.removeAttribute('data-lb-contrast');
    });
  };
})();

Bigger audits — use one of these

This bookmarklet is a quick triage tool. For a thorough accessibility audit covering more than just colour contrast, use one of these — they're free, well-maintained, and built by accessibility specialists who do this for a living. We're not affiliated with any of them.

  • axe DevTools — browser extension. Covers most automatable WCAG checks. The de-facto standard.
  • WAVE — WebAIM's tool. Visual overlay style, good for showing clients where issues are.
  • Lighthouse — built into Chrome DevTools (Application → Lighthouse). Covers performance, SEO, and accessibility in one pass.

For e-learning specifically, also try our WCAG Course Audit Checklist — a manual checklist tailored to course-specific concerns (captions, drag-and-drops, time-limited assessments) that automated tools can\'t verify.