← Back to Blog
Accessibility 📅 2 Mar 2026 ⏱ 13 min read

Building Accessible Web Components: The Complete WCAG Guide

Semantic HTML, ARIA patterns, keyboard navigation, focus trapping, colour contrast, and screen reader testing — build for every user from day one.

Why Accessibility Cannot Be an Afterthought

1 in 4 adults has some form of disability. Inaccessible websites create direct barriers to healthcare, financial services, and employment. WCAG compliance is also a legal requirement in many jurisdictions under ADA, EN 301 549, and similar legislation. And critically — accessibility improvements consistently benefit all users, not just those with disabilities.

Semantic HTML: The Free 80 Percent

<!-- Wrong: no keyboard, no role -->
<div class="btn" onclick="submit()">Save</div>
<!-- Right: keyboard, role, form submission built-in -->
<button type="submit">Save</button>

<!-- Wrong: no landmark -->
<div class="nav">...</div>
<!-- Right: navigable landmark -->
<nav aria-label="Main navigation">...</nav>

<!-- Informative image -->
<img src="product.webp" alt="Blue ceramic mug 350ml">
<!-- Decorative image -->
<img src="bg.svg" alt="">

ARIA: Use Only When Necessary

The First Rule of ARIA: if a native HTML element provides the same semantics, use it. Misused ARIA creates worse accessibility than no ARIA by overriding correct native semantics.

<button aria-expanded="false" aria-controls="panel-1">
  Section heading
</button>
<div id="panel-1" role="region" aria-labelledby="trigger-1" hidden>
  Content...
</div>

<!-- Live regions for dynamic updates -->
<div aria-live="polite" id="status"></div>
<script>document.getElementById('status').textContent = '3 results found';</script>

Keyboard Navigation Patterns

Standard patterns keyboard users rely on: Tab moves forward, Shift+Tab moves backward, Enter activates buttons/links, Space activates buttons/checkboxes, Arrow keys navigate within composite widgets (menus, tabs, radio groups), Escape closes modals and menus. Never invent custom patterns.

Focus Trapping for Modals

function trapFocus(modal) {
  const sel = 'a[href],button:not([disabled]),input,select,textarea,[tabindex]:not([tabindex="-1"])';
  const els = [...modal.querySelectorAll(sel)];
  const first = els[0], last = els[els.length-1];
  modal.addEventListener('keydown', e => {
    if (e.key !== 'Tab') return;
    if (e.shiftKey && document.activeElement===first) {
      e.preventDefault(); last.focus();
    } else if (!e.shiftKey && document.activeElement===last) {
      e.preventDefault(); first.focus();
    }
  });
  first.focus();
}
function closeModal(modal, trigger) {
  modal.hidden = true;
  trigger.focus(); // return focus to opener
}

Colour Contrast

WCAG 2.1 AA requires 4.5:1 for normal text, 3:1 for large text (18pt+ or 14pt+ bold), and 3:1 for UI boundaries and focus indicators. Common failures: placeholder text (often 2:1), text on gradients, and deliberately muted secondary copy.

Visible Focus Indicators

:focus:not(:focus-visible) { outline: none; }
:focus-visible {
  outline: 2px solid #6366f1;
  outline-offset: 3px;
  border-radius: 4px;
}

Accessible Forms

<label for="email">Email address</label>
<input type="email" id="email" autocomplete="email"
       aria-describedby="email-hint">
<p id="email-hint">We'll send your receipt here.</p>

<!-- Error state -->
<input aria-invalid="true" aria-describedby="email-err">
<p id="email-err" role="alert">Enter a valid email address.</p>

Accessibility is a quality standard, not a feature. Build with semantic HTML, test every component with a keyboard, and you will solve the majority of issues before users ever encounter them.

Share: 𝕏 Twitter in LinkedIn
← Previous
CSS Custom Properties: Variables, Theming, and @property
Next →
JavaScript Async Patterns: Promises, async/await, and Beyond