We've built the semantic base (04-01), the forms (04-02) and the declarative ARIA layer (04-03). What's left is the most visual part of the Perceivable principle, and it's almost all CSS: that text has enough contrast, that information isn't conveyed by color alone, that content can be enlarged up to 200% without breaking, that it flows to narrow screens without horizontal scroll, that it respects text spacing, and that focus is always visible. These are criteria 1.4.3, 1.4.11, 1.4.1, 1.4.4, 1.4.10, 1.4.12 and 2.4.7/2.4.11 that in Module 3 we studied as a requirement and now implement in CSS.

We'll apply it to Cursalia's brand palette —reviewing a color that fails and fixing it— and thinking very much about Sofía, who has low vision and browses with the zoom at 300%.

Contents

  1. WCAG contrast ratios (1.4.3 and 1.4.11)
  2. How to measure contrast
  3. Not relying on color alone (1.4.1)
  4. Text resizing to 200% and relative units (1.4.4)
  5. Reflow to 320 CSS px (1.4.10)
  6. Text spacing (1.4.12)
  7. Visible focus indicator (2.4.7 / 2.4.11)
  8. Reviewing Cursalia's brand palette
  9. Common mistakes and tips
  10. Exercises
  11. Conclusion

WCAG contrast ratios (1.4.3 and 1.4.11)

Contrast is the difference in luminance between two colors, expressed as a ratio from 1:1 (none) to 21:1 (black on white). WCAG sets minimums at AA level:

Element Minimum AA ratio Criterion
Normal text (< 24 px, or < 18.66 px if bold) 4.5 : 1 1.4.3
Large text (≥ 24 px, or ≥ 18.66 px bold) 3 : 1 1.4.3
UI components and graphics (input borders, icons, a button's boundary) 3 : 1 1.4.11

Criterion 1.4.11 (non-text contrast) is the one usually forgotten: a form field's border, a card's icon or the line of a progress chart must also stand out from the background by at least 3:1. An input whose only border is a very light gray on white is invisible to Sofía even if its text contrasts well.

/* INCORRECT: gray on white ≈ 2.8:1, doesn't reach 4.5:1 */
.text { color: #999999; background: #ffffff; }

/* CORRECT: dark gray on white ≈ 7:1 */
.text { color: #595959; background: #ffffff; }

How to measure contrast

You don't estimate it "by eye": you measure it. Common tools (the detail of testing is Module 6):

  • Browser DevTools: when you inspect text, the color picker shows the ratio and whether it passes AA/AAA.
  • WebAIM Contrast Checker: you paste the two colors and it gives the ratio.
  • Extensions like axe DevTools or Lighthouse, which audit the whole page.

The practical rule: every pair (text color, background color) in the palette must be verified. And watch out for text over images or gradients: if the background varies, you must guarantee contrast in the least favorable area (a semi-transparent layer behind the text solves it).

Not relying on color alone (1.4.1)

Criterion 1.4.1 requires that no information be conveyed solely by color. Marta (who can't see) and anyone with color blindness would lose that data. You must always add a second channel: text, an icon, a shape or a pattern.

Typical cases in Cursalia:

  • Form errors (seen in 04-02): not just a red border → add "Error:" text and an icon.
  • Links within a paragraph: if they're only distinguished by color, someone who doesn't perceive that color won't know they're links → underline them or give them another visible cue.
  • Course progress chart: if "completed / in progress / locked" is encoded only with green/yellow/gray, add a text label, an icon or a pattern.
/* INCORRECT: links are only distinguished by color */
a { color: #1a6feb; text-decoration: none; }

/* CORRECT: besides color, an underline (a second visible channel) */
a { color: #1a6feb; text-decoration: underline; }
<!-- INCORRECT: the course state lives only in the color class -->
<li class="green">Module 1</li>
<li class="gray">Module 2</li>

<!-- CORRECT: color + text + icon (three channels) -->
<li class="completed"><span aria-hidden="true">✔</span> Module 1 — Completed</li>
<li class="locked"><span aria-hidden="true">🔒</span> Module 2 — Locked</li>

Text resizing to 200% and relative units (1.4.4)

Criterion 1.4.4 requires that text can be enlarged up to 200% without losing content or functionality. The number-one cause of failure is setting font sizes in absolute pixels: font-size: 16px doesn't respond well to the user's size preference.

The solution is to use relative units:

  • rem: relative to the document's base size (the one the user configures in their browser). Ideal for font sizes.
  • em: relative to the parent element's size. Useful for proportional spacing.
  • Avoid px for typography and for containers that must grow with the text.
/* INCORRECT: absolute px, ignores the user's preference */
body { font-size: 16px; }
h1   { font-size: 32px; }

/* CORRECT: rem/em respect the configured base size */
html { font-size: 100%; }        /* respects the browser preference */
body { font-size: 1rem; }        /* ≈ 16px, but scalable */
h1   { font-size: 2rem; }        /* grows proportionally */

If Sofía raises the base font size to 200%, with rem all the text grows coherently. With px it wouldn't move. Also, don't set rigid heights in px on text containers: a height: 40px on a button causes enlarged text to be cut off; use min-height and let the padding breathe.

Reflow to 320 CSS px (1.4.10)

Criterion 1.4.10 (Reflow) requires that content can be viewed without scrolling in two dimensions at a width equivalent to 320 CSS px (which happens, for example, when Sofía zooms to 400% on a 1280 px monitor). That is: vertical scroll is fine (normal), but not horizontal. Content must reflow, not spill over.

This is achieved with responsive/fluid design:

/* INCORRECT: fixed width that causes horizontal scroll when enlarged */
.catalog { width: 1200px; }
.card    { width: 380px; float: left; }

/* CORRECT: fluid grid that reflows to one column on narrow screens */
.catalog {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1rem;
  max-width: 100%;
}
img { max-width: 100%; height: auto; }   /* images that don't overflow */

minmax(280px, 1fr) with auto-fill makes the catalog cards go from several columns to a single one without anything spilling off the side. Also use @media queries to reorganize the layout at small widths. Criterion exception: content that needs two dimensions (large tables, maps) may require scrolling, but that's the exception, not the norm.

Text spacing (1.4.12)

Criterion 1.4.12 requires that, if the user forces certain spacing (to improve readability, something that helps Hugo with dyslexia a lot), content doesn't break or overlap. The values your CSS must support:

Property Value the user may impose
line-height (line spacing) at least 1.5 × font size
Space between paragraphs at least 2 × font size
letter-spacing at least 0.12 × font size
word-spacing at least 0.16 × font size

The key to meeting it: don't set rigid heights or use overflow: hidden on text containers. If a button or card has a fixed height, increasing the line spacing cuts off the text.

/* CORRECT: comfortable base and containers that grow with the text */
p       { line-height: 1.6; margin-bottom: 1.5em; }
.card   { min-height: 8rem; overflow: visible; }  /* min-height, not height */

Visible focus indicator (2.4.7 / 2.4.11)

When Lucía tabs with the keyboard, she needs to see where the focus is at all times (criterion 2.4.7, Focus Visible; and 2.4.11 reinforces that the indicator not be covered). The browser draws a default outline... and the classic mistake is removing it for aesthetics:

/* INCORRECT AND SERIOUS: removes focus giving nothing in return */
:focus { outline: none; }

With that, Lucía is left "blind": she tabs without knowing which control she's on. If by design you want to change the focus style, replace it, never simply remove it. The modern tool is :focus-visible, which shows the ring when navigating with the keyboard but not when clicking with the mouse:

/* CORRECT: custom focus, clearly visible and with good contrast */
:focus-visible {
  outline: 3px solid #1a6feb;   /* clear thickness */
  outline-offset: 2px;          /* separation so the border doesn't cover it */
  border-radius: 4px;
}

Requirements of the indicator: it must have sufficient contrast (at least 3:1 against its surroundings, in line with 1.4.11), be thick and not be hidden behind other elements. We link with the keyboard criteria 2.4.7/2.4.11; programmatic focus handling when opening dialogs and widgets is Module 5.

Reviewing Cursalia's brand palette

Let's put it all into practice with Cursalia's palette. The corporate color is a brand blue used for buttons and links. We audit:

Use Text color Background Ratio Passes AA (4.5:1)?
Primary button #ffffff #4a9cf5 (light brand blue) 2.3 : 1 ❌ No
Primary button (fixed) #ffffff #1a6feb (darkened blue) 4.6 : 1 ✅ Yes
Gray hint text #9a9a9a #ffffff 2.8 : 1 ❌ No
Hint text (fixed) #595959 #ffffff 7 : 1 ✅ Yes

The light brand blue looks nice, but with white text on top it doesn't reach 4.5:1: Sofía can't read it comfortably. The fix doesn't force a change of identity: you darken the blue just enough to comply, reserving the original light tone for decorative backgrounds where there's no text on top.

/* INCORRECT: brand blue too light for white text (2.3:1) */
.btn-primary { background: #4a9cf5; color: #fff; }

/* CORRECT: darkened blue that passes AA (4.6:1) while keeping the identity */
.btn-primary {
  background: #1a6feb;
  color: #fff;
  border: 1px solid #1a6feb;   /* also, non-text contrast of the border: 1.4.11 */
}
.btn-primary:focus-visible {
  outline: 3px solid #0b3d91;
  outline-offset: 2px;
}

Common Mistakes and Tips

  • Trusting your eye for contrast. Always measure it with a tool; light grays deceive.
  • Forgetting non-text contrast (1.4.11). Input borders, icons and chart lines also need 3:1.
  • Encoding information with color only. Always add a second channel: text, icon, underline, pattern.
  • Typography in px. It prevents scaling; use rem/em and respect the user's base size.
  • Fixed heights on text containers. They break resizing (1.4.4) and spacing (1.4.12): use min-height and avoid overflow: hidden.
  • Fixed widths that cause horizontal scroll. Design with fluid grids and max-width: 100% to meet reflow (1.4.10).
  • outline: none with no replacement. One of the most damaging mistakes for keyboard users. Use :focus-visible with a contrasted ring.
  • Tip: test your page with browser zoom at 200% and 400%, and traverse it with the keyboard. If horizontal scroll appears, text is cut off or you lose sight of the focus, you have work to do.

Exercises

Exercise 1: fix insufficient contrast

Cursalia's "discount" label uses white text #ffffff on a light orange #ffb066, with a ratio of 1.8:1. Explain why it fails and propose a fix that keeps orange as the color, indicating which additional channel you would add.

Exercise 2: from px to relative units

Refactor this CSS so the text scales to 200% and the button doesn't cut off the text when enlarged.

body { font-size: 14px; }
.cta {
  font-size: 18px;
  height: 36px;
  line-height: 36px;
  overflow: hidden;
}

Exercise 3: accessible visible focus

A designer put :focus { outline: none; } on the entire student dashboard SPA "because the blue border is ugly". Explain the problem and write an accessible alternative with :focus-visible.

Solutions

Solution 1. It fails because 1.8:1 is far below the AA minimum of 4.5:1 for normal text (and even below 3:1 for large text): Sofía can't distinguish the text from the background. Fix: darken the orange until you achieve contrast with the text (or put dark text on the light orange), and don't rely on color alone for the label by adding the word "Discount" or an icon:

/* Dark text on the light orange: ≈ 6:1 */
.discount { background: #ffb066; color: #3d2400; }
<span class="discount"><span aria-hidden="true">🏷️</span> Discount -20%</span>

Solution 2. You replace px with rem, and the fixed height with padding + min-height, removing the overflow: hidden:

body { font-size: 100%; }     /* respects the user's base size */
.cta {
  font-size: 1.125rem;        /* ≈ 18px, but scalable */
  min-height: 2.5rem;         /* grows if needed, doesn't cut */
  line-height: 1.5;
  padding: 0.5rem 1rem;
  overflow: visible;
}

Now, when enlarging to 200% or forcing more line spacing (1.4.12), the button grows with the text instead of clipping it.

Solution 3. The problem: outline: none leaves Lucía (and anyone using a keyboard) unable to tell which element has focus, breaking 2.4.7. In a SPA with many controls, it's especially serious. Alternative: a custom, visible and contrasted indicator, only for keyboard navigation:

:focus-visible {
  outline: 3px solid #1a6feb;
  outline-offset: 2px;
  border-radius: 4px;
}

The aesthetics are preserved (no ring appears when clicking with the mouse) without sacrificing keyboard accessibility.

Conclusion

With this lesson we close Module 4. You've implemented the visual side of "Perceivable" in CSS: contrast ratios 4.5:1 / 3:1 (text and non-text), not relying on color alone, text scalable with relative units up to 200%, reflow without horizontal scroll at 320 CSS px, text spacing that doesn't break the layout, and a visible focus with :focus-visible that is never removed without a replacement. And you've fixed Cursalia's palette so that Sofía, with her zoom at 300%, reads without barriers.

With this we now have a solid, static base: semantic HTML (04-01), accessible forms (04-02), declarative ARIA (04-03) and accessible CSS (04-04). Everything we've done describes what each thing is and how it looks, but we haven't yet given it behavior. That's the leap of Module 5: bringing interactive widgets to life with JavaScript (the filters dropdown, the FAQ accordion), managing the keyboard and focus programmatically, and making multimedia (video and audio) and images accessible with the alt attribute in depth. We move from structure to behavior.

© Copyright 2026. All rights reserved