Francisco Guardado Blog

My WCAG Tutorial
My WCAG Tutorial

Quick Background: What Are WCAG and Section 508? WCAG (Web Content Accessibility Guidelines) is the international standard for web accessibility, published by the W3C. Current versions are WCAG 2.1 and 2.2, each with three conformance levels: A, AA, and AAA. Section 508 is a U.S. federal law requiring government agencies (and often their contractors) to make electronic content accessible. Since 2017, Section 508 has been updated to directly reference WCAG 2.0 Level AA as its technical standard. The practical takeaway: if you build to WCAG 2.1/2.2 Level AA, you satisfy Section 508 and most enterprise accessibility policies at the same time.

WCAG is organized around four principles, often remembered by the acronym POUR: -Perceivable: Users must be able to perceive the content (not hidden from any of their senses) -Operable: Users must be able to operate the interface (keyboard, voice, switch devices, etc.) -Understandable: Content and operation must be understandable -Robust: Content must work with a wide range of assistive technologies, now and in the future

Submit

Prefer:

HTML
<button type="submit">Submit</button>

Same logic applies to navigation, lists, and page structure:

HTML
<body>
  <a href="#main-content" class="skip-link">Skip to main content</a>
  <header>... nav with 15 links ...</header>
  <main id="main-content">
    ...
  </main>
</body>
CSS
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
}

.skip-link:focus {
  left: 0;
  padding: 1rem;
  background: #fff;
  outline: 3px solid #1a73e8;
}

(WCAG 2.4.1 Bypass Blocks)

CSS
/* Bad: fixed pixel sizing ignores user's browser font-size settings */
body {
  font-size: 14px;
}

/* Good: rem units scale with the user's browser/OS preferences */
body {
  font-size: 1rem; /* 16px default, but respects user zoom/settings */
}

.container {
  max-width: 100%;
  overflow-x: hidden;
}

Also avoid disabling pinch-zoom on mobile:

HTML
<!-- Bad: blocks users who need to zoom in -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">

<!-- Good -->
<meta name="viewport" content="width=device-width, initial-scale=1">

(WCAG 1.4.4 – Resize Text, 1.4.10 – Reflow)

HTML
<video controls>
  <source src="demo.mp4" type="video/mp4">
  <track kind="captions" src="demo-captions.vtt" srclang="en" label="English" default>
</video>

(WCAG 1.2.2 Captions (Prerecorded))

HTML
<!-- Bad -->
<a href="/report.pdf">Click here</a> to download the report.

<!-- Good -->
<a href="/report.pdf">Download the Q3 accessibility audit report (PDF)</a>

A Practical Testing Checklist Before shipping any feature, I run through this: Unplug your mouse. Tab through the entire page or flow. Can you reach and operate everything? Is focus always visible? Run an automated scanner. Use axe DevTools, Lighthouse, or WAVE. These catch about 30 to 40% of issues, such as missing alt text, contrast failures, and missing labels, but they will not catch logical or UX issues. Test with a screen reader. Use VoiceOver on Mac, NVDA on Windows, or TalkBack on Android. Simply landing on your homepage and navigating by headings can surface many accessibility issues. Zoom to 200%. Check that nothing breaks, overlaps, or gets cut off. Check color contrast. Test every text, background, and UI component combination. Validate forms. Submit the form with errors and confirm the error is announced to the user, not just displayed visually.

Rule of thumb: if a native HTML element (

<div class="form-field">
  <label for="email">Email address</label>
  <input
    type="email"
    id="email"
    name="email"
    required
    aria-describedby="email-error"
    aria-invalid="true"
  />
  <span id="email-error" role="alert">
    Please enter a valid email address, e.g. name@example.com
  </span>
</div>

Where ARIA genuinely helps with dynamic regions, custom widgets, live status updates:

(WCAG 4.1.2 Name, Role, Value)

XML / MARKUP
<!-- Decorative image: hide it from assistive tech -->
<img src="divider-swirl.png" alt="">

<!-- Informative image: describe its purpose, not its appearance -->
<img src="chart-q3-sales.png" alt="Q3 sales rose 24% compared to Q2">

<!-- Functional image (e.g. inside a link/button) -->
<a href="/cart">
  <img src="cart-icon.svg" alt="View shopping cart">
</a>
XML / MARKUP
<!-- Bad: color is the only signal -->
<span style="color: red;">Invalid</span>

<!-- Good: icon + text + color -->
<span class="error">
  <svg aria-hidden="true">...</svg> Invalid: must be a valid email address
</span>
XML / MARKUP
// Bad: div "buttons" aren't keyboard-focusable at all
<div onClick={openModal}>Open Settings</div>

// Good: native button gets focus, Enter/Space, and semantics for free
<button onClick={openModal}>Open Settings</button>
HTML
function CustomToggle({ pressed, onToggle, children }) {
  return (
    <div
      role="button"
      tabIndex={0}
      aria-pressed={pressed}
      onClick={onToggle}
      onKeyDown={(e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          onToggle();
        }
      }}
    >
      {children}
    </div>
  );
}
XML / MARKUP
/* This breaks keyboard navigation for everyone */
*:focus {
  outline: none;
}
HTML
<header>
  <nav aria-label="Primary">
    <ul>
      <li><a href="/">Home</a></li>
      <li><a href="/projects">Projects</a></li>
      <li><a href="/contact">Contact</a></li>
    </ul>
  </nav>
</header>

<main>
  <h1>Page Title</h1>
  <article>...</article>
</main>

<footer>...</footer>
XML / MARKUP
button:focus-visible {
  outline: 3px solid #1a73e8;
  outline-offset: 2px;
}
XML / MARKUP
<!-- Bad: skips from h1 to h3, and uses h4 just because it "looked right" -->
<h1>Portfolio</h1>
<h3>Featured Projects</h3>
<h4 style="font-size: 24px;">E-commerce App</h4>

<!-- Good: sequential, based on document structure not visual size -->
<h1>Portfolio</h1>
<h2>Featured Projects</h2>
<h3>E-commerce App</h3>

Meaningful Alt Text for Images Every needs an alt attribute. But the content of that attribute matters more than its presence.

Color Contrast You Can Actually Prove Text needs a contrast ratio of at least: 4.5:1 for normal text (Level AA) 3:1 for large text (18pt+/14pt bold+) 3:1 for UI components and graphical objects (icons, form borders, focus indicators)

Full Keyboard Operability Every interactive element must be reachable and usable with the Tab, Shift+Tab, Enter, and Space keys alone, no mouse. This is the single most common failure I see in portfolios and production apps.

Visible Focus Indicators Never

Logical Heading Structure Headings aren't for styling text size they build a document outline that screen reader users can navigate (many jump straight from heading to heading).

Accessible Forms This is where most real-world sites fail. Every input needs a programmatically associated label, and errors need to be announced, not just shown visually.

Key points:

HTML
<!-- Announces status changes (e.g. "Saved", "3 items in cart") -->
<div role="status" aria-live="polite">
  {statusMessage}
</div>

<!-- Modal dialog: traps focus and is announced correctly -->
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
  <h2 id="dialog-title">Confirm Deletion</h2>
  ...
</div>
HTML
<!-- Unnecessary and risky: reinventing a native element badly -->
<div role="button" aria-label="Close" onclick="closeModal()"></div>

<!-- Just use the real thing -->
<button aria-label="Close" onclick="closeModal()">✕</button>
HTML
/* Fails AA: light gray on white, ~2.3:1 */
.text-muted {
  color: #aaaaaa;
  background-color: #ffffff;
}

/* Passes AA: ~4.6:1 */
.text-muted {
  color: #757575;
  background-color: #ffffff;
}