EduFlow

Section 4 · Visual Design and Content

Color and Contrast

18 min3 min readCode playground

Lesson at a glance

Measure and fix text and interface contrast against WCAG 2.2 ratios, stop relying on colour alone, and understand why APCA isn’t part of the standard yet.

  1. 1The contrast ratio
  2. 2The thresholds you need
  3. 3Don’t rely on colour alone
  4. 4What about APCA?

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Low-contrast text is the most common accessibility failure on the web: WebAIM found it on 83.9% of the top million home pages in 2026. It is also the easiest to measure, because contrast is a number. Ticketly’s designers love pale grey on white and white on pastel orange. Maha, at 400% zoom with low vision, reads neither, and neither does anyone checking a ticket on a phone in the sun.

The contrast ratio

WCAG compares the relative luminance of two colours, a measure of perceived brightness from 0 (black) to 1 (white). The ratio runs from 1:1 (identical colours) to 21:1 (black on white). Here is the calculation, the same one every contrast checker runs:

JavaScript
function luminance(hex) {
  const [r, g, b] = [1, 3, 5].map((i) => {
    const c = parseInt(hex.slice(i, i + 2), 16) / 255;
    return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function contrast(a, b) {
  const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
  return ((hi + 0.05) / (lo + 0.05)).toFixed(2);
}

console.log('price #9a9a9a on white:', contrast('#9a9a9a', '#ffffff'));
console.log('price #767676 on white:', contrast('#767676', '#ffffff'));
console.log('white on orange button:', contrast('#ffffff', '#f4a261'));
console.log('ink on orange button:  ', contrast('#1b1830', '#f4a261'));

The green channel dominates luminance, which is why white text on orange or yellow fails so badly even when it looks bright.

The thresholds you need

What Level AA Criterion
Normal text 4.5:1 1.4.3 Contrast (Minimum)
Large text: at least 24px, or 18.66px bold 3:1 1.4.3
Input borders, focus indicators, icons, chart lines needed to understand content 3:1 1.4.11 Non-text Contrast
Text, enhanced 7:1 1.4.6 (AAA)

Exemptions are narrow: logos, purely decorative elements and disabled controls. Hint text inside inputs, “secondary” grey captions, footer links and text over photos all count. For text over an image, add a solid or semi-opaque overlay behind the text and measure against the lightest part of the image it can sit on.

Ticketly’s fixes:

ticketly/styles.css
.price { color: #595959; }                 /* was #9a9a9a: 2.81:1 → 7.00:1 */
.btn { background: #f4a261; color: #1b1830; } /* was white text: 2.06:1 → 8.35:1 */
input { border: 1px solid #77708f; }        /* was #ddd: 1.36:1 → 4.67:1 */

The orange button keeps its brand colour; only the text colour changed. That is usually the cheapest fix.

Don’t rely on colour alone

WCAG 1.4.1 Use of Color (A) asks that colour is never the only way to convey information. About 1 in 12 men and 1 in 200 women have some form of colour vision deficiency, most often red-green.

On Ticketly that rules out:

  • Links in body text that differ from surrounding text only by colour. Keep the underline (or ensure 3:1 against the body text plus another cue on hover and focus; the underline is far simpler).
  • Errors shown only as a red border. Add a text message and an icon, connected as you did in Accessible Forms.
  • Seat availability as green and red dots. Add a pattern or a label: “available”, “taken”.
Left: seat dots that differ only by colour look identical in greyscale. Right: available seats are outlined circles and taken seats are filled circles with a cross, still distinct without colour, with a legend in words. Colour only Which seats are free? Shape + label available taken
Same information, with and without colour

What about APCA?

You will meet the Accessible Perceptual Contrast Algorithm (APCA) in design tools. It models perception better than the WCAG 2 ratio: it accounts for font size and weight, and treats light-on-dark differently from dark-on-light, which matters for dark mode. It has been explored for WCAG 3, which is still a W3C working draft.

APCA is not part of WCAG 2.2, and no law references it. An auditor will test your pages with the 4.5:1 and 3:1 ratios. Use APCA as an extra aid if your team likes it, especially for thin fonts and dark themes, but every colour pair must still pass the WCAG 2 ratio.

Strong contrast helps people see Ticketly at its default size. The next lesson asks what happens when they change that size, and what happens when the page moves.

Key takeaways

  • Body text needs a contrast ratio of at least 4.5:1 with its background; large text (24px, or 18.66px bold) needs 3:1.
  • Borders of inputs, focus indicators, icons and chart lines that people need to see require 3:1 against their surroundings (1.4.11 Non-text Contrast).
  • Colour can’t be the only way information is shown; pair it with text, an icon, an underline or a pattern.
  • APCA is a promising perceptual contrast model explored for WCAG 3, but WCAG 2.2 and the laws that cite it use the 4.5:1 and 3:1 ratios.

Further reading

Check your understanding

4 questions

Question 1 of 4

Ticketly’s prices are #767676 on white, a ratio of 4.54:1, set at 16px regular. Does that pass 1.4.3 Contrast (Minimum)?

Question 2 of 4

A form field’s border is #dddddd on a white page (1.36:1). The label text has excellent contrast. What fails?

Question 3 of 4

Ticketly’s checkout marks invalid fields with a red border and nothing else. Who is most clearly failed by this design?

Question 4 of 4

A designer proposes checking every colour pair with APCA instead of the WCAG 2 contrast ratio. What should you tell them?

Practice

Code playground

Fix the Weekend pass colours

Edit only the CSS so the Weekend pass card meets WCAG 2.2 AA:

  • .price text needs at least 4.5:1 against the white background.
  • The .btn text needs at least 4.5:1 against the button’s background (you may keep the orange).
  • The quantity input border needs at least 3:1 against the card background.
  • The link inside .note differs from the paragraph only by weight; give it a non-colour cue such as an underline, and keep its text at 4.5:1.

The checks compute real contrast ratios from your computed styles, so any passing colours are accepted.

<article class="pass">
  <h2>Weekend pass</h2>
  <p class="price">SAR 320 for both nights</p>
  <p class="note">Includes entry to the <a href="#lounge">harbour lounge</a> and a free drink token.</p>
  <label for="qty">Quantity</label>
  <input id="qty" type="number" min="1" max="6" value="1">
  <button type="button" class="btn">Add to basket</button>
</article>Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Preview

Console

(no output)

Checks

  • Price text reaches 4.5:1 against its background
  • Button text reaches 4.5:1 against the button colour
  • The input border reaches 3:1 against the card background
  • The link has an underline (or bottom border) and 4.5:1 text contrast