HTML and CSS course Β· Module 10: Accessibility (a11y)

Color Contrast and Typography

8 min read
In this lesson8

You open the museum page on your phone in full sunlight by the Nile and see light gray text on a white background that is impossible to read. A visually impaired person always sees it that way. In Ancient Egypt, hieroglyphs were painted in vivid colors on contrasting backgrounds - not for decoration, but so they could be read even in dark temple chambers. Today the same principle applies in web design: content must be readable for everyone.

Contrast Ratio

The contrast ratio compares the luminance of two colors and ranges from 1:1 (the same color) to 21:1 (black on white). WCAG defines minimum contrast requirements between text and background:

LevelNormal textLarge text (24px or about 18.7px bold)
AA4.5:13:1
AAA7:14.5:1

According to WCAG, large text means at least 18 points (about 24px) or 14 points in bold. For normal text at level AA, remember one number: 4.5:1.

Contrast Examples

Look at three pairs of colors - a good one, a bad one and a fixed one. You can check the values from the comments in any contrast tool:

1/* GOOD - high contrast */
2.good-contrast {
3    color: #1a1a2e;            /* Dark text */
4    background-color: #f5f1e3; /* Light background */
5    /* Contrast: ~15:1 - meets AAA */
6}
7
8/* BAD - too low contrast */
9.bad-contrast {
10    color: #999999;            /* Gray text */
11    background-color: #cccccc; /* Gray background */
12    /* Contrast: ~1.8:1 - does not meet even AA */
13}
14
15/* FIXED */
16.fixed-contrast {
17    color: #595959;            /* Darker gray */
18    background-color: #ffffff; /* White background */
19    /* Contrast: ~7:1 - meets AA and even AAA */
20}

The fix did not change the character of the page - it was enough to darken the text and lighten the background. Contrast depends on both colors, so always check the pair.

Do Not Rely Solely on Color

Color cannot be the only way to convey information - people with color blindness may not distinguish certain colors, especially red and green. Compare a field whose error is marked by color alone with one that says it in three ways:

1<!-- BAD - only color indicates error -->
2<style>
3    .error-field { border-color: red; }
4</style>
5<input type="email" class="error-field">
6
7<!-- GOOD - color + icon + text -->
8<style>
9    .error-field {
10        border: 2px solid #d32f2f;
11        background-image: url('error-icon.svg');
12        background-repeat: no-repeat;
13        background-position: right 8px center;
14    }
15</style>
16<input type="email" class="error-field"
17       aria-invalid="true"
18       aria-describedby="email-err">
19<p id="email-err" class="error-msg">
20    <span aria-hidden="true">&#10060;</span>
21    Please enter a valid email address
22</p>

The red border stays, but an icon and a text message joined to the field with aria-describedby are added. The cross has aria-hidden="true", because the reader will read the message next to it anyway.

Color does not reach a screen reader either: when the ticket status turns from green to red, a blind person notices nothing - that is why the change is announced by an aria-live region, which you know from the ARIA lesson:

1<div aria-live="polite" id="ticket-status">
2    Only 3 tickets left for today
3</div>

The value polite waits until the reader finishes its current sentence, assertive interrupts it immediately, and off (the default when the attribute is missing) announces nothing. role="alert" works as a shortcut for aria-live="assertive" - save it for urgent messages.

Accessible Typography

Text Size

Set text size in relative units. rem is calculated from the font size of the html element, which is 16px by default and depends on the user's browser settings:

1/* Use relative units */
2body {
3    font-size: 100%; /* = 16px by default */
4}
5
6h1 { font-size: 2rem; }      /* 32px */
7h2 { font-size: 1.5rem; }    /* 24px */
8p  { font-size: 1rem; }      /* 16px */
9small { font-size: 0.875rem; } /* 14px - minimum! */
10
11/* NEVER block text zooming */
12/* BAD */
13html { font-size: 16px !important; }
14/* GOOD */
15html { font-size: 100%; }

When someone sets a larger font in their browser, a page in rem grows along with it, while a rigid 16px !important ignores their choice. WCAG does not set a minimum font size, but 16px for body text is a sensible standard.

Line Height and Spacing

WCAG success criterion 1.4.12 (level AA) requires that content is not cut off or overlapping when the user increases spacing to the values below. The simplest approach is to design with that margin from the start:

1/* Values from WCAG criterion 1.4.12: */
2p {
3    line-height: 1.5;        /* Line height: 1.5x font size */
4    letter-spacing: 0.12em;  /* Letter spacing */
5    word-spacing: 0.16em;    /* Word spacing */
6}
7
8/* Paragraph spacing >= 2x font size */
9p + p {
10    margin-top: 2em;
11}

This is a robustness threshold, not a rigid rule - in the exercises you will also meet a clear, though smaller, spacing of margin-top: 1.5em.

Text Width

Very long lines tire the eyes, because it is hard to find the start of the next line. The ch unit is the width of the "0" character in the current font:

1/* Optimal line width: 50-80 characters */
2.article-content {
3    max-width: 70ch; /* 70 characters */
4    margin: 0 auto;
5}

So the column holds about 70 characters, and margin: 0 auto centers it. At level AAA (criterion 1.4.8), WCAG speaks of a maximum of 80 characters per line.

Contrast Checking Tools

The CSS specification describes functions that pick a contrasting color on their own: the experimental color-contrast() and the simpler contrast-color(), but browser support for them is still limited. So check contrast in the Chrome DevTools color picker or in the WebAIM Contrast Checker. Colors are easiest to manage in CSS variables, which media queries swap for different modes:

1/* Use CSS variables for consistency */
2:root {
3    --text-primary: #1a1a2e;
4    --text-secondary: #4a4a5a;
5    --bg-primary: #ffffff;
6    --bg-secondary: #f5f1e3;
7    --accent: #8b6914;
8    --error: #d32f2f;
9    --success: #2e7d32;
10}
11
12/* High contrast mode */
13@media (prefers-contrast: more) {
14    :root {
15        --text-primary: #000000;
16        --text-secondary: #1a1a1a;
17        --bg-primary: #ffffff;
18        --accent: #6b4c00;
19    }
20}
21
22/* Dark mode */
23@media (prefers-color-scheme: dark) {
24    :root {
25        --text-primary: #e0e0e0;
26        --text-secondary: #b0b0b0;
27        --bg-primary: #1a1a2e;
28        --bg-secondary: #2d2d44;
29    }
30}

Dark mode does not require rewriting any selectors - only the values in :root change. Still, check every new color pair again, because light text on a dark background also needs 4.5:1. Older materials use the value high, but browsers recognize prefers-contrast: more.

prefers-contrast and forced-colors

Two more media queries react to system settings: prefers-contrast detects a request for more contrast, and forced-colors a mode in which the system imposes its own palette, like the contrast themes in Windows:

1/* High contrast mode */
2@media (prefers-contrast: more) {
3    button {
4        border: 3px solid currentColor;
5    }
6}
7
8/* Forced colors mode (e.g., Windows High Contrast) */
9@media (forced-colors: active) {
10    .card {
11        border: 2px solid ButtonText;
12    }
13
14    a {
15        color: LinkText;
16    }
17
18    /* Decorative backgrounds are removed - add borders */
19    .highlight {
20        forced-color-adjust: none;
21        /* OR preserve original colors */
22    }
23}

In this mode the browser overrides text, background and border colors with the system palette, so an element highlighted only by its background may disappear - a border will survive. ButtonText and LinkText are system colors, and forced-color-adjust: none turns forcing off - use it rarely.

Focus Outline Contrast

Contrast is not only about text. WCAG criterion 1.4.11 (level AA) requires at least 3:1 for interface components and their states, including the :focus-visible outline from the keyboard navigation lesson. Gold #d4af37 stands out beautifully against dark navy (about 8:1), but on a white background it reaches only about 2.1:1, so choose a darker shade there.

Contrast and Typography Checklist

  1. Text-to-background contrast: minimum 4.5:1 (AA) or 7:1 (AAA)
  2. Font size: minimum 16px (1rem) for body
  3. Line height: minimum 1.5
  4. Line width: 50-80 characters
  5. Do not rely solely on color
  6. Support prefers-contrast and prefers-color-scheme
  7. Use relative units (rem, em)

Just as ancient Egyptians ensured the readability of hieroglyphs painted on temple walls, so must we ensure the readability of text on our pages - so that everyone can read our message, regardless of conditions. In the next lesson you will check all of this with tools.

Practice

Compare the good and bad color pairs in the editor below, then change them and check the contrast in DevTools:

Code for this lesson: index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Color contrast and typography</title>
7    <link rel="stylesheet" href="style.css">
8</head>
9<body>
10    <h1>Contrast and typography</h1>
11
12    <section>
13        <h2>Contrast examples</h2>
14
15        <div class="contrast-box good-contrast">
16            <h3>Good contrast (12:1)</h3>
17            <p>Dark text on a light background - readable for everyone.</p>
18        </div>
19
20        <div class="contrast-box bad-contrast">
21            <h3>Poor contrast (1.8:1)</h3>
22            <p>Gray text on a gray background - almost unreadable!</p>
23        </div>
24
25        <div class="contrast-box fixed-contrast">
26            <h3>Improved contrast (5.9:1)</h3>
27            <p>A darker gray on a white background - meets WCAG AA.</p>
28        </div>
29    </section>
30
31    <section>
32        <h2>Do not rely on color alone</h2>
33
34        <div class="example-bad">
35            <h3>Bad: Only color signals the error</h3>
36            <input type="email" class="error-only-color" value="invalid">
37        </div>
38
39        <div class="example-good">
40            <h3>Good: Color + icon + text</h3>
41            <input type="email" class="error-with-icon"
42                   value="invalid"
43                   aria-invalid="true"
44                   aria-describedby="email-err">
45            <p id="email-err" class="error-msg">
46                Γ— Enter a valid email address
47            </p>
48        </div>
49    </section>
50
51    <section>
52        <h2>Accessible typography</h2>
53        <article class="readable-text">
54            <h3>Optimally formatted text</h3>
55            <p>This text has the right line height (1.6), font size (1rem = 16px)
56               and optimal line width (max 70 characters). All these parameters
57               are important for readability and meet the WCAG requirements.</p>
58            <p>The second paragraph has the right spacing from the first one (2em),
59               which makes longer texts easier to read.</p>
60        </article>
61    </section>
62
63    <section>
64        <h2>CSS variables and themes</h2>
65        <div class="theme-demo">
66            <p>This text uses CSS variables (Custom Properties) and automatically
67               adapts to the user's preferences: dark mode,
68               high contrast.</p>
69        </div>
70    </section>
71</body>
72</html>

Remember: a hieroglyph painted in gold on sand is beautiful, but nobody can read it - contrast is the paint that lets every pilgrim see the message.

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What minimum contrast does WCAG 2.1 Level AA require for NORMAL text?

  2. 2. What is the minimum text-to-background contrast ratio required by WCAG AA for normal text?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Improve color contrast (change light text to darker), add icons next to colored statuses, and create CSS variables with dark mode.

  • Code editor

    Improve typography: change font-size to 100%, set line-height to 1.5, max-width to 70ch, and add letter-spacing, word-spacing, and margin-top for paragraphs.

  • Click in order

    Arrange the correct CSS media query syntax for dark mode:

  • Vertical ordering

    Arrange the correct CSS syntax for a visible focus indicator:

  • Click in order

    Arrange the correct syntax of a div element with an aria-live region:

  • Code editor

    Add aria-live="polite" to the ticket status region, role="alert" for urgent notifications, and aria-live="assertive" to the countdown timer.

  • Vertical ordering

    Arrange aria-live values from lowest to highest priority:

Useful articles