Contrast ratio

명암 대비

The WCAG formula that scores foreground/background brightness difference from 1:1 to 21:1. 4.5:1 or higher passes AA for normal text.

Also known as: WCAG contrast대비율
···
html
<div class="wrap">
  <div class="patch" id="patch"><span id="patchText">Aa</span></div>
  <div class="stats">
    <div class="ratio" id="ratio">4.50:1</div>
    <div class="badges">
      <span class="badge" id="bAA">AA</span>
      <span class="badge" id="bAAlg">AA Large</span>
      <span class="badge" id="bAAA">AAA</span>
    </div>
  </div>
</div>
css
.wrap{width:min(440px,92%);display:grid;gap:14px;justify-items:center}
.patch{width:100%;height:clamp(64px,18vmin,110px);border-radius:14px;display:grid;place-items:center;border:1px solid var(--line);transition:background .6s;cursor:pointer}
#patchText{font:800 clamp(20px,6.4vmin,36px)/1 system-ui}
.stats{display:grid;gap:8px;justify-items:center}
.ratio{font:800 clamp(16px,4.4vmin,22px)/1 ui-monospace,monospace;color:var(--fg)}
.badges{display:flex;gap:8px}
.badge{padding:4px 10px;border-radius:999px;font:700 clamp(9px,2.4vmin,11px)/1 ui-monospace,monospace;border:1px solid var(--line);opacity:.35;color:var(--muted)}
.badge.pass{opacity:1;background:var(--accent-3);color:#04241c;border-color:transparent}
js
const PAIRS = [
  ['#1b1b1f', '#ffffff'],
  ['#ffffff', '#2563eb'],
  ['#0f172a', '#facc15'],
  ['#6b7280', '#f3f4f6'],
  ['#f43f5e', '#111827'],
  ['#111827', '#22c55e'],
];
let idx = 0;
function hexToRgb(hex) { hex = hex.replace('#', ''); return [0, 2, 4].map(i => parseInt(hex.substr(i, 2), 16)); }
function channel(v) { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }
function luminance(hex) { const c = hexToRgb(hex).map(channel); return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]; }
function ratio(a, b) { const la = luminance(a) + 0.05, lb = luminance(b) + 0.05; return la > lb ? la / lb : lb / la; }

const patch = document.getElementById('patch'), patchText = document.getElementById('patchText');
const ratioEl = document.getElementById('ratio'), bAA = document.getElementById('bAA'), bAAlg = document.getElementById('bAAlg'), bAAA = document.getElementById('bAAA');

function render() {
  const [fg, bg] = PAIRS[idx];
  patch.style.background = bg;
  patchText.style.color = fg;
  const r = ratio(fg, bg);
  ratioEl.textContent = r.toFixed(2) + ':1';
  bAA.classList.toggle('pass', r >= 4.5);
  bAAlg.classList.toggle('pass', r >= 3);
  bAAA.classList.toggle('pass', r >= 7);
}
render();
patch.addEventListener('click', () => { idx = (idx + 1) % PAIRS.length; render(); });
setInterval(() => { idx = (idx + 1) % PAIRS.length; render(); }, 2400);

Human eyes don't perceive brightness linearly, so you can't just average RGB channels. WCAG gamma-corrects each sRGB channel to linear light, computes relative luminance as 0.2126R + 0.7152G + 0.0722B, adds 0.05 to both the lighter and darker luminance, and divides: (L1+0.05)/(L2+0.05).

There are three thresholds: 4.5:1 for AA on normal text, 3:1 for AA on large text (18pt+, or 14pt bold), and 7:1 for AAA on normal text. Pure black on pure white tops out at the theoretical maximum, 21:1.

Every "contrast checker" plugin in every design tool runs this exact formula — running the numbers instead of eyeballing a color pair is the single biggest reducer of accessibility incidents.

When to use

Check it before shipping any color pair that carries meaning — text, icons, form borders.