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.
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)();- 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.
- Firefox: right-click the bookmarks toolbar → New Bookmark…. Paste the URL into Location, type a name, save.
- 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
- Open the page you want to check (your course, an LMS preview, anything).
- Click the Check Contrast bookmark.
- A panel appears in the top-right. Failing elements are outlined in red. Click any item in the list to scroll to it.
- 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.