liveBook · Manning

Contrast ratio

As explained in Math for Web Design

A contrast ratio is a numerical comparison of the relative luminances of two colors, calculated with the higher luminance as the numerator.

Contrast ratio turns a familiar UI question—“Can people read this text?”—into a number. It compares the relative luminance, or perceived brightness, of two colors so that text and its background can be checked against WCAG accessibility thresholds rather than judged by eye. section 7.4 section 7.4.1

Calculate it from the lighter luminance L1 and darker luminance L2: CR = (L1 + 0.05) / (L2 + 0.05). Luminance runs from 0 for perceived black to 1 for perceived white, so the result runs from 1:1 for identical brightness to 21:1 for black and white. Always place the higher luminance first; swapping foreground and background does not change the result. section 7.4.2 section 7.4.3

Use the threshold that matches the text treatment:

  • Normal text below 18px needs at least 4.5:1 for AA or 7:1 for AAA. section 7.4.1
  • Large text—18px or larger, or bold at 14px or larger—needs at least 3:1 for AA or 4.5:1 for AAA. section 7.4.1
  • A medium gray, hsl(0 0% 46%), on white reaches about 4.54:1: enough for normal-text AA, but not AAA. A darker hsl(0 0% 35%) reaches 7:1. section 7.4.1

Why hue alone is not enough

Pure blue (#0000FF) has relative luminance 0.0722. Against white (L = 1), its ratio is 8.59:1, exceeding AAA. Against pure red (L = 0.2126), the ratio is only 2.15:1, failing AA. The same blue can therefore be accessible or inaccessible depending on its paired color. section 7.4.3

Relative luminance is not a simple average of RGB channels. Convert RGB values to normalized values, gamma-correct them into linear light, then apply weighted coefficients: 0.2126 for red, 0.7152 for green, and 0.0722 for blue. Those weights reflect differing visual sensitivity across colors. section 7.4.3 section 7.4.4

Listing 7.10 JavaScript code to calculate a contrast ratio
function hexToRgb(hex) {  #1
    hex = hex.replace(/^#/, "");    #2
    if (hex.length === 3) {    #3
        hex = hex.split("").map(ch => ch + ch).join("");     #3
    }     #3
    const num = parseInt(hex, 16);
    return {
        r: (num >> 16) & 255,    #4
        g: (num >> 8) & 255,     #4
        b: num & 255     #4
    };
}
function relativeLuminance(r, g, b) {    #5
    let R_n = r / 255, G_n = g / 255, B_n = b / 255;    #6
    const toLinear = (c) =>    #7
        c <= 0.04045 ?      #7
        c / 12.92 :      #7
        ((c + 0.055) / 1.055) ** 2.4;     #7
    const R_l = toLinear(R_n);     #7
    const G_l = toLinear(G_n);     #7
    const B_l = toLinear(B_n);     #7
    return 0.2126 * R_l + 0.7152 * G_l + 0.0722 * B_l;    #8
}
function getContrastRatio(color1, color2) {    #9
    const rgb1 = hexToRgb(color1);
    const rgb2 = hexToRgb(color2);
    const L1 = relativeLuminance(rgb1.r, rgb1.g, rgb1.b);
    const L2 = relativeLuminance(rgb2.r, rgb2.g, rgb2.b);

    const lighter = Math.max(L1, L2);    #10
    const darker = Math.min(L1, L2);     #10

    const ratio = (lighter + 0.05) / (darker + 0.05);    #11
    return ratio;
}
function checkCompliance(cr, isLargeText = false) {    #12
    const thresholds = {
        aa: { normal: 4.5, large: 3.0 },    #13
        aaa: { normal: 7.0, large: 4.5 }     #13
    };

    const textType = isLargeText ? 'large' : 'normal';

    return {
        aa: cr >= thresholds.aa[textType],    #14
        aaa: cr >= thresholds.aaa[textType]    #15
    };
}

Use the calculator functions to convert 3- or 6-digit hex colors, derive relative luminance, compute the ratio, and check AA and AAA compliance. This makes contrast checking practical for values returned by a color input. section 7.4.4

Contrast checks also matter when a light/dark-mode toggle changes the UI. Do not treat dark mode as a simple inversion: preserve readable text, separation between backgrounds, and the prominence of accents by maintaining or exceeding the required ratios. section 7.5

Practical takeaway: measure the actual foreground/background pair, use the higher luminance in the numerator, and evaluate the resulting number against the threshold for that text size and weight. section 7.4.1 section 7.4.2

How Math for Web Design covers it

How deep it goes
Derives ratios from gamma-corrected relative luminance and implements calculation and compliance checks in JavaScript.
Examples
Contrasts blue against white and red, compares grays on white, and builds a hex-color calculator.
What it assumes
Introduces the needed luminance math while using RGB hex colors, CSS text sizing, and JavaScript functions.
What you can do after
Apply it via JavaScript to calculate color-pair ratios and report AA or AAA compliance.
How it connects
Connects relative luminance, gamma correction, WCAG, CSS colors, JavaScript, and dark mode.

Also explained in

Compare how 3 books explain contrast ratio →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Math for Web Design. The section links point to the parts of the book it draws on.