Semantic HTML, ARIA patterns, keyboard navigation, focus trapping, colour contrast, and screen reader testing — build for every user from day one.
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.
<!-- 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="">
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>
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.
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
}
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.
:focus:not(:focus-visible) { outline: none; }
:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 3px;
border-radius: 4px;
}
<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.