UI Components Medium
Color Contrast Checker
Real-time WCAG contrast ratio checker that evaluates color pairs against AA and AAA standards with live preview.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #e5e5e5;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.checker {
width: 100%;
max-width: 560px;
}
.checker-title {
font-size: 1.75rem;
font-weight: 800;
letter-spacing: -0.02em;
margin-bottom: 0.375rem;
}
.checker-sub {
color: #737373;
font-size: 0.875rem;
margin-bottom: 2rem;
line-height: 1.5;
}
/* ── Color inputs ── */
.color-inputs {
display: flex;
align-items: flex-end;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.color-field {
flex: 1;
}
.color-field label {
display: block;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #a3a3a3;
margin-bottom: 0.5rem;
}
.input-row {
display: flex;
align-items: center;
gap: 0.5rem;
background: #171717;
border: 1px solid #262626;
border-radius: 0.5rem;
padding: 0.375rem 0.625rem;
}
.input-row input[type="color"] {
width: 28px;
height: 28px;
border: none;
border-radius: 0.25rem;
cursor: pointer;
padding: 0;
background: none;
-webkit-appearance: none;
}
.input-row input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.input-row input[type="color"]::-webkit-color-swatch {
border: 1px solid #404040;
border-radius: 0.25rem;
}
.input-row input[type="text"] {
flex: 1;
background: none;
border: none;
color: #e5e5e5;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 0.875rem;
outline: none;
width: 80px;
}
.swap-btn {
background: #171717;
border: 1px solid #262626;
border-radius: 0.5rem;
color: #a3a3a3;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.15s, color 0.15s;
flex-shrink: 0;
}
.swap-btn:hover {
background: #262626;
color: #e5e5e5;
}
/* ── Icon buttons (copy/paste) ── */
.icon-btn {
background: none;
border: none;
color: #525252;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 0.25rem;
transition: color 0.15s, background 0.15s;
flex-shrink: 0;
position: relative;
}
.icon-btn:hover {
color: #e5e5e5;
background: #262626;
}
.icon-btn.copied {
color: #22c55e;
}
.icon-btn .tooltip {
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: #262626;
color: #e5e5e5;
font-size: 0.6875rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
}
.icon-btn .tooltip.show {
opacity: 1;
}
/* ── Preview ── */
.preview {
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid #262626;
transition: background-color 0.2s, color 0.2s;
}
.preview-normal {
font-size: 1rem;
line-height: 1.6;
margin-bottom: 0.75rem;
}
.preview-large {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.preview-link {
text-decoration: underline;
font-size: 0.9375rem;
}
/* ── Ratio display ── */
.ratio-display {
display: flex;
align-items: center;
justify-content: space-between;
background: #171717;
border: 1px solid #262626;
border-radius: 0.75rem;
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
}
.ratio-label {
font-size: 0.875rem;
font-weight: 600;
color: #a3a3a3;
}
.ratio-value {
font-size: 1.75rem;
font-weight: 800;
font-family: "SF Mono", "Fira Code", monospace;
}
/* ── Results grid ── */
.results-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 2rem;
}
.result-card {
background: #171717;
border: 1px solid #262626;
border-radius: 0.75rem;
padding: 1rem;
transition: border-color 0.2s;
}
.result-card.pass {
border-color: #22c55e;
}
.result-card.fail {
border-color: #ef4444;
}
.result-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.result-level {
font-size: 0.75rem;
font-weight: 700;
background: #262626;
padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
letter-spacing: 0.04em;
}
.result-type {
font-size: 0.8125rem;
color: #a3a3a3;
}
.result-req {
font-size: 0.75rem;
color: #525252;
margin-bottom: 0.625rem;
}
.result-badge {
font-size: 0.875rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.375rem;
}
.result-badge.pass {
color: #22c55e;
}
.result-badge.fail {
color: #ef4444;
}
/* ── Presets ── */
.presets-title {
font-size: 0.875rem;
font-weight: 700;
color: #a3a3a3;
margin-bottom: 0.75rem;
}
.preset-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.5rem;
}
.preset-btn {
display: flex;
align-items: center;
gap: 0.5rem;
background: #171717;
border: 1px solid #262626;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
cursor: pointer;
color: #e5e5e5;
font-size: 0.75rem;
font-weight: 500;
transition: background 0.15s, border-color 0.15s;
}
.preset-btn:hover {
background: #1f1f1f;
border-color: #404040;
}
.preset-swatch {
width: 20px;
height: 20px;
border-radius: 0.25rem;
border: 1px solid #404040;
flex-shrink: 0;
position: relative;
overflow: hidden;
}
.preset-swatch-inner {
position: absolute;
inset: 0;
display: flex;
}
.preset-swatch-fg,
.preset-swatch-bg {
flex: 1;
}
@media (max-width: 480px) {
.color-inputs {
flex-direction: column;
align-items: stretch;
}
.swap-btn {
align-self: center;
transform: rotate(90deg);
}
.results-grid {
grid-template-columns: 1fr;
}
}(function () {
// ── Elements ──
var fgPicker = document.getElementById("fg-picker");
var fgInput = document.getElementById("fg-color");
var bgPicker = document.getElementById("bg-picker");
var bgInput = document.getElementById("bg-color");
var swapBtn = document.getElementById("swap-btn");
var preview = document.getElementById("preview");
var ratioEl = document.getElementById("ratio-value");
var presetList = document.getElementById("preset-list");
var badges = {
aaNormal: document.getElementById("aa-normal-badge"),
aaLarge: document.getElementById("aa-large-badge"),
aaaNormal: document.getElementById("aaa-normal-badge"),
aaaLarge: document.getElementById("aaa-large-badge"),
};
var cards = {
aaNormal: document.getElementById("aa-normal"),
aaLarge: document.getElementById("aa-large"),
aaaNormal: document.getElementById("aaa-normal"),
aaaLarge: document.getElementById("aaa-large"),
};
// ── Presets ──
var presets = [
{ name: "White / Black", fg: "#ffffff", bg: "#000000" },
{ name: "Cream / Navy", fg: "#faf7f2", bg: "#1a1a2e" },
{ name: "Green / Dark", fg: "#22c55e", bg: "#0a0a0a" },
{ name: "Sky / Slate", fg: "#38bdf8", bg: "#0f172a" },
{ name: "Amber / Brown", fg: "#fbbf24", bg: "#1c1917" },
{ name: "Gray / White", fg: "#6b7280", bg: "#ffffff" },
{ name: "Red / Dark", fg: "#ef4444", bg: "#0a0a0a" },
{ name: "Purple / Deep", fg: "#a78bfa", bg: "#0c0a1a" },
];
// ── WCAG luminance helpers ──
function hexToRgb(hex) {
hex = hex.replace("#", "");
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
var n = parseInt(hex, 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
function linearize(c) {
var s = c / 255;
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
}
function relativeLuminance(rgb) {
var r = linearize(rgb[0]);
var g = linearize(rgb[1]);
var b = linearize(rgb[2]);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function contrastRatio(hex1, hex2) {
var l1 = relativeLuminance(hexToRgb(hex1));
var l2 = relativeLuminance(hexToRgb(hex2));
var lighter = Math.max(l1, l2);
var darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
function isValidHex(val) {
return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(val);
}
// ── Update UI ──
function update() {
var fg = fgInput.value;
var bg = bgInput.value;
if (!isValidHex(fg) || !isValidHex(bg)) return;
// Sync pickers
fgPicker.value = fg.length === 4 ? "#" + fg[1] + fg[1] + fg[2] + fg[2] + fg[3] + fg[3] : fg;
bgPicker.value = bg.length === 4 ? "#" + bg[1] + bg[1] + bg[2] + bg[2] + bg[3] + bg[3] : bg;
// Preview
preview.style.backgroundColor = bg;
preview.style.color = fg;
var links = preview.querySelectorAll("a");
for (var i = 0; i < links.length; i++) links[i].style.color = fg;
// Ratio
var ratio = contrastRatio(fg, bg);
ratioEl.textContent = ratio.toFixed(2) + " : 1";
// Color the ratio based on quality
if (ratio >= 7) ratioEl.style.color = "#22c55e";
else if (ratio >= 4.5) ratioEl.style.color = "#facc15";
else if (ratio >= 3) ratioEl.style.color = "#f97316";
else ratioEl.style.color = "#ef4444";
// Check levels
var checks = {
aaNormal: ratio >= 4.5,
aaLarge: ratio >= 3,
aaaNormal: ratio >= 7,
aaaLarge: ratio >= 4.5,
};
for (var key in checks) {
var pass = checks[key];
badges[key].textContent = pass ? "\u2713 Pass" : "\u2717 Fail";
badges[key].className = "result-badge " + (pass ? "pass" : "fail");
cards[key].className = "result-card " + (pass ? "pass" : "fail");
}
}
// ── Copy / Paste helpers ──
function showTooltip(btn, text) {
var tip = btn.querySelector(".tooltip");
if (!tip) {
tip = document.createElement("span");
tip.className = "tooltip";
btn.appendChild(tip);
}
tip.textContent = text;
tip.classList.add("show");
btn.classList.add("copied");
setTimeout(function () {
tip.classList.remove("show");
btn.classList.remove("copied");
}, 1200);
}
function copyColor(btn, input) {
navigator.clipboard.writeText(input.value).then(function () {
showTooltip(btn, "Copied!");
});
}
function pasteColor(btn, input) {
navigator.clipboard
.readText()
.then(function (text) {
text = text.trim();
if (!text.startsWith("#")) text = "#" + text;
if (isValidHex(text)) {
input.value = text;
update();
showTooltip(btn, "Pasted!");
} else {
showTooltip(btn, "Invalid hex");
}
})
.catch(function () {
showTooltip(btn, "No access");
});
}
var fgCopy = document.getElementById("fg-copy");
var fgPaste = document.getElementById("fg-paste");
var bgCopy = document.getElementById("bg-copy");
var bgPaste = document.getElementById("bg-paste");
fgCopy.addEventListener("click", function () {
copyColor(fgCopy, fgInput);
});
fgPaste.addEventListener("click", function () {
pasteColor(fgPaste, fgInput);
});
bgCopy.addEventListener("click", function () {
copyColor(bgCopy, bgInput);
});
bgPaste.addEventListener("click", function () {
pasteColor(bgPaste, bgInput);
});
// ── Events ──
fgPicker.addEventListener("input", function () {
fgInput.value = fgPicker.value;
update();
});
bgPicker.addEventListener("input", function () {
bgInput.value = bgPicker.value;
update();
});
fgInput.addEventListener("input", update);
bgInput.addEventListener("input", update);
swapBtn.addEventListener("click", function () {
var tmp = fgInput.value;
fgInput.value = bgInput.value;
bgInput.value = tmp;
update();
});
// ── Build presets ──
presets.forEach(function (p) {
var btn = document.createElement("button");
btn.className = "preset-btn";
btn.innerHTML =
'<span class="preset-swatch"><span class="preset-swatch-inner">' +
'<span class="preset-swatch-fg" style="background:' +
p.fg +
'"></span>' +
'<span class="preset-swatch-bg" style="background:' +
p.bg +
'"></span>' +
"</span></span>" +
p.name;
btn.addEventListener("click", function () {
fgInput.value = p.fg;
bgInput.value = p.bg;
update();
});
presetList.appendChild(btn);
});
// ── Init ──
update();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Color Contrast Checker</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="checker">
<h1 class="checker-title">Color Contrast Checker</h1>
<p class="checker-sub">Evaluate color pairs against WCAG AA & AAA standards in real time.</p>
<!-- Color inputs -->
<div class="color-inputs">
<div class="color-field">
<label for="fg-color">Foreground</label>
<div class="input-row">
<input type="color" id="fg-picker" value="#ffffff" aria-label="Pick foreground color" />
<input type="text" id="fg-color" value="#ffffff" maxlength="7" spellcheck="false" aria-label="Foreground hex" />
<button class="icon-btn" id="fg-copy" aria-label="Copy foreground color" title="Copy">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</button>
<button class="icon-btn" id="fg-paste" aria-label="Paste foreground color" title="Paste from clipboard">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/></svg>
</button>
</div>
</div>
<button class="swap-btn" id="swap-btn" aria-label="Swap foreground and background colors">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 16V4m0 0L3 8m4-4l4 4"/><path d="M17 8v12m0 0l4-4m-4 4l-4-4"/></svg>
</button>
<div class="color-field">
<label for="bg-color">Background</label>
<div class="input-row">
<input type="color" id="bg-picker" value="#1a1a2e" aria-label="Pick background color" />
<input type="text" id="bg-color" value="#1a1a2e" maxlength="7" spellcheck="false" aria-label="Background hex" />
<button class="icon-btn" id="bg-copy" aria-label="Copy background color" title="Copy">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</button>
<button class="icon-btn" id="bg-paste" aria-label="Paste background color" title="Paste from clipboard">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/></svg>
</button>
</div>
</div>
</div>
<!-- Live preview -->
<div class="preview" id="preview">
<p class="preview-normal">The quick brown fox jumps over the lazy dog. (Normal text — 16px)</p>
<p class="preview-large">Large text sample (24px bold)</p>
<a href="#" class="preview-link" onclick="return false">This is a link example</a>
</div>
<!-- Contrast ratio -->
<div class="ratio-display">
<span class="ratio-label">Contrast Ratio</span>
<span class="ratio-value" id="ratio-value">—</span>
</div>
<!-- Results grid -->
<div class="results-grid">
<div class="result-card" id="aa-normal">
<div class="result-header">
<span class="result-level">AA</span>
<span class="result-type">Normal Text</span>
</div>
<div class="result-req">Requires 4.5 : 1</div>
<div class="result-badge" id="aa-normal-badge">—</div>
</div>
<div class="result-card" id="aa-large">
<div class="result-header">
<span class="result-level">AA</span>
<span class="result-type">Large Text</span>
</div>
<div class="result-req">Requires 3 : 1</div>
<div class="result-badge" id="aa-large-badge">—</div>
</div>
<div class="result-card" id="aaa-normal">
<div class="result-header">
<span class="result-level">AAA</span>
<span class="result-type">Normal Text</span>
</div>
<div class="result-req">Requires 7 : 1</div>
<div class="result-badge" id="aaa-normal-badge">—</div>
</div>
<div class="result-card" id="aaa-large">
<div class="result-header">
<span class="result-level">AAA</span>
<span class="result-type">Large Text</span>
</div>
<div class="result-req">Requires 4.5 : 1</div>
<div class="result-badge" id="aaa-large-badge">—</div>
</div>
</div>
<!-- Presets -->
<div class="presets">
<h2 class="presets-title">Preset Pairs</h2>
<div class="preset-list" id="preset-list"></div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Real-time WCAG contrast ratio checker that calculates relative luminance and contrast ratio between any two colors. Evaluates against AA (4.5:1 normal, 3:1 large) and AAA (7:1 normal, 4.5:1 large) standards with instant visual feedback and preset color pairs.