|$ curl https://forge-ai.dev/api/markdown?path=docs/html/accessibility
$cat docs/web-accessibility.md
updated Recently·35 min read·published

Web Accessibility

HTMLAccessibilitya11yIntermediate
Introduction

Web accessibility (a11y) ensures that people with disabilities can perceive, understand, navigate, and interact with the web. It is not a feature — it is a fundamental requirement of the web. Approximately 15% of the global population has some form of disability, and inaccessible design excludes them from digital experiences.

The WCAG (Web Content Accessibility Guidelines) define three conformance levels: A (minimum), AA (standard — legal requirement in most regions), and AAA (advanced). The four POUR principles — Perceivable, Operable, Understandable, Robust — form the foundation of all accessibility work.

Semantic HTML

Semantic HTML is the single most impactful accessibility technique. Screen readers and assistive technologies rely on element semantics to convey meaning and structure to users. A well-structured document with semantic elements often requires zero ARIA.

Landmark regions help screen reader users navigate the page quickly. Browsers and assistive technologies expose these landmarks for shortcut navigation:

LandmarkHTML ElementPurpose
Banner<header>Site-oriented content (logo, search)
Navigation<nav>Navigation links
Main<main>Primary content (one per page)
Complementary<aside>Supporting content
Content Info<footer>Copyright, privacy links
Region<section>Topic with heading
Form<form>Collection of form controls
Search<form role="search">Search functionality
semantic.html
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>Accessible Page</title>
7</head>
8<body>
9 <header>
10 <nav aria-label="Main navigation">
11 <ul>
12 <li><a href="/">Home</a></li>
13 <li><a href="/products">Products</a></li>
14 <li><a href="/about">About</a></li>
15 <li><a href="/contact">Contact</a></li>
16 </ul>
17 </nav>
18 </header>
19
20 <main>
21 <h1>Welcome to Our Site</h1>
22 <p>This page uses semantic landmarks for accessibility.</p>
23
24 <section aria-labelledby="features-heading">
25 <h2 id="features-heading">Features</h2>
26 <ul>
27 <li>Fast and reliable</li>
28 <li>Accessible by design</li>
29 <li>Open source</li>
30 </ul>
31 </section>
32 </main>
33
34 <footer>
35 <p>&copy; 2026 Accessible Corp</p>
36 </footer>
37</body>
38</html>

best practice

Use aria-labelledby to associate a section with its heading explicitly. This helps screen reader users understand the purpose of each region when navigating by landmark. Every <section> should have a heading.
ARIA — Accessible Rich Internet Applications

ARIA is a specification that defines attributes to enhance accessibility when native HTML semantics are insufficient. It provides roles, states, and properties that tell assistive technologies about element behavior and current state.

ARIA Roles

Roles describe the type of widget or structure an element represents. The main categories are widget, structure, landmark, and live region roles.

CategoryRolesUsage
Widgetbutton, link, checkbox, radio, slider, tab, menuitemInteractive UI components
Structureheading, list, listitem, table, row, cell, imgDocument structure elements
Landmarkbanner, navigation, main, complementary, contentinfo, form, search, regionPage regions for navigation
Live Regionalert, log, marquee, status, timerDynamic content updates

ARIA States & Properties

ARIA attributes describe the current state of an element or provide additional context. They begin with aria-:

AttributeValuesDescription
aria-expandedtrue, false, undefinedIndicates if a collapsible element is open
aria-controlsID referenceReferences the element being controlled
aria-labelstringProvides an accessible name
aria-labelledbyID referenceReferences element that labels this one
aria-describedbyID referenceReferences element with a description
aria-hiddentrue, falseHides element from accessibility tree
aria-currentpage, step, location, date, time, true, falseIndicates the current item in a set
aria-liveoff, polite, assertivePoliteness of dynamic content announcements
aria-atomictrue, falseAnnounce region as whole or by changes
aria-relevantadditions, removals, text, allWhat types of changes are relevant
aria-busytrue, falseElement is being updated
aria-requiredtrue, falseIndicates required form field
aria-invalidtrue, false, grammar, spellingIndicates input validation error
aria-disabledtrue, falseElement is disabled but still visible
aria-examples.html
HTML
1<!-- Tab widget with ARIA -->
2<div role="tablist" aria-label="Documentation tabs">
3 <button role="tab" aria-selected="true" aria-controls="panel-1"
4 id="tab-1" tabindex="0">HTML</button>
5 <button role="tab" aria-selected="false" aria-controls="panel-2"
6 id="tab-2" tabindex="-1">CSS</button>
7 <button role="tab" aria-selected="false" aria-controls="panel-3"
8 id="tab-3" tabindex="-1">JS</button>
9</div>
10
11<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
12 <p>HTML content here.</p>
13</div>
14<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
15 <p>CSS content here.</p>
16</div>
17<div role="tabpanel" id="panel-3" aria-labelledby="tab-3" hidden>
18 <p>JS content here.</p>
19</div>
20
21<!-- Current page in navigation -->
22<nav aria-label="Breadcrumb">
23 <ol>
24 <li><a href="/">Home</a></li>
25 <li><a href="/docs">Docs</a></li>
26 <li><span aria-current="page">Accessibility</span></li>
27 </ol>
28</nav>
29
30<!-- Live region for notifications -->
31<div aria-live="polite" aria-atomic="true" class="notification">
32</div>
ARIA Rules

The first rule of ARIA is: do not use ARIA if a native HTML element exists that provides the semantics and behavior you need. Native elements have built-in keyboard handling, focus management, and accessibility mappings that ARIA cannot fully replicate.

aria-first-rule.html
HTML
1<!-- BAD: div with ARIA button -->
2<div role="button" tabindex="0" onclick="submit()">
3 Submit
4</div>
5<!-- Problems: no Enter key handling in some browsers, no native form submission,
6 no default button styling, requires manual focus management -->
7
8<!-- GOOD: native button -->
9<button type="button" onclick="submit()">Submit</button>
10<!-- Built-in: click, keyboard, focus, form behavior, accessibility -->

The second rule: no ARIA is better than bad ARIA. Incorrect ARIA can actively harm accessibility by providing misleading information to screen readers.

bad-aria.html
HTML
1<!-- BAD: incorrect ARIA that breaks expectations -->
2<div role="heading">
3 This looks like a heading but lacks proper level
4</div>
5<!-- Screen reader announces "heading" but cannot navigate by level -->
6
7<!-- BAD: redundant ARIA -->
8<button role="button">Click Me</button>
9<!-- Native button already has role="button" -- redundant -->
10
11<!-- BAD: missing keyboard support -->
12<div role="checkbox" aria-checked="false" onclick="toggle()">
13 Accept terms
14</div>
15<!-- Space key must also toggle the checkbox -->
16<!-- Use native <input type="checkbox"> instead -->

warning

The first rule of ARIA is the most important. Before adding ARIA, ask: "Can I use a native HTML element instead?" If the answer is yes, use the native element. ARIA is a polyfill for when HTML falls short, not a replacement for it.
Keyboard Navigation

Many users navigate the web using only a keyboard. Ensuring full keyboard operability is a WCAG 2.1 Level A requirement (Success Criterion 2.1.1).

ConceptDescription
tabindexControls tab order: 0 (natural), -1 (focusable via JS only), positive (explicit order — avoid)
Focus managementProgrammatically moving focus when content changes (e.g., dialog opens, route changes)
Skip linksFirst focusable element, jumps to main content, bypassing navigation
focus-visibleCSS pseudo-class showing focus ring only for keyboard users, not mouse clicks
Roving tabindexPattern for composite widgets (tabs, menus, listboxes) — only one element is tabbable
keyboard.css
CSS
1/* Visible focus indicator — required by WCAG 2.1 */
2:focus-visible {
3 outline: 2px solid #00FF41;
4 outline-offset: 2px;
5 border-radius: 2px;
6}
7
8/* Hide focus ring from mouse users but keep it for keyboard */
9:focus:not(:focus-visible) {
10 outline: none;
11}
12
13/* Skip link — visually hidden until focused */
14.skip-link {
15 position: absolute;
16 top: -100%;
17 left: 0;
18 z-index: 1000;
19 padding: 8px 16px;
20 background: #00FF41;
21 color: #000;
22 font-weight: 600;
23}
24.skip-link:focus {
25 top: 0;
26}
keyboard.html
HTML
1<!-- Skip link — first element in body -->
2<a href="#main-content" class="skip-link">
3 Skip to main content
4</a>
5
6<!-- Roving tabindex example: tabs -->
7<div role="tablist" aria-label="Tabs">
8 <button role="tab" aria-selected="true" tabindex="0">Tab 1</button>
9 <button role="tab" aria-selected="false" tabindex="-1">Tab 2</button>
10 <button role="tab" aria-selected="false" tabindex="-1">Tab 3</button>
11</div>
12
13<!-- Focus management with JavaScript -->
14<script>
15function openDialog(dialog) {
16 dialog.showModal();
17 // Move focus into the dialog
18 dialog.querySelector('[autofocus]').focus();
19}
20
21function closeDialog(dialog) {
22 const returnFocus = document.querySelector('[data-open-dialog]');
23 dialog.close();
24 returnFocus.focus(); // Return focus to trigger element
25}
26</script>
🔥

pro tip

Never use tabindex values greater than 0. Positive tabindex values create a confusing tab order that is difficult to maintain. Use the natural DOM order with tabindex="0" for interactive elements and tabindex="-1" for scripted focus management.
Screen Readers

Screen readers (JAWS, NVDA, VoiceOver, TalkBack) convert digital text to synthesized speech or braille. They navigate by traversing the accessibility tree, which is derived from the DOM but augmented with ARIA.

Key practices for screen reader compatibility:

screen-readers.html
HTML
1<!-- alt text — most important SR practice -->
2<!-- Informative image: describe the content -->
3<img src="chart.png"
4 alt="Bar chart showing Q1 sales: Jan $5K, Feb $7K, Mar $6K" />
5
6<!-- Decorative image: hide from screen readers -->
7<img src="divider.svg" alt="" role="presentation" />
8<!-- Or use: alt="" (empty alt = hidden from SR) -->
9
10<!-- Link text must make sense out of context -->
11<!-- BAD -->
12<a href="/docs/guide">click here</a> for the guide.
13
14<!-- GOOD -->
15<a href="/docs/guide">Read the full documentation guide</a>.
16
17<!-- Heading hierarchy — never skip levels -->
18<h1>Page Title</h1>
19 <h2>Section</h2>
20 <h3>Subsection</h3>
21 <h2>Another Section</h2>
22
23<!-- Use aria-label for icon-only buttons -->
24<button aria-label="Close dialog">
25 <svg aria-hidden="true" focusable="false">...</svg>
26</button>
27
28<!-- Hidden headings for structure -->
29<section aria-labelledby="section-heading">
30 <h2 id="section-heading" class="sr-only">Contact Form</h2>
31 <!-- form content -->
32</section>
preview
Color & Contrast

WCAG 2.1 requires minimum contrast ratios for text and UI components. Insufficient contrast is one of the most common accessibility failures on the web.

LevelNormal TextLarge Text (18px+ bold or 24px+)UI Components
AA4.5:13:13:1
AAA7:14.5:1
contrast.css
CSS
1/* WCAG AA compliant color combinations */
2.text-primary {
3 color: #00FF41; /* on #0D0D0D = 11.5:1 ✓ AAA */
4 background: #0D0D0D;
5}
6
7.text-body {
8 color: #A0A0A0; /* on #0D0D0D = 7.2:1 ✓ AAA */
9 background: #0D0D0D;
10}
11
12.text-muted {
13 color: #808080; /* on #0D0D0D = 4.7:1 ✓ AA */
14 background: #0D0D0D;
15}
16
17.text-disabled {
18 color: #525252; /* on #0D0D0D = 3.1:1 ✗ FAIL */
19 background: #0D0D0D;
20 /* Only use for decorative text — not for interactive or important content */
21}
22
23/* Don't rely on color alone — use icons and text */
24.error-message {
25 color: #FF6B6B;
26}
27.error-message::before {
28 content: "⚠ "; /* Adds warning symbol beside color */
29}

best practice

Never rely on color alone to convey information. Color-blind users (8% of men) may not perceive color differences. Always combine color with text labels, icons, patterns, or underlines. Tools like WebAIM's Contrast Checker and axe DevTools can verify your ratios.
Forms Accessibility

Accessible forms are critical for users of all abilities. Every form control must have a programmatically associated label, clear error messaging, and appropriate ARIA states.

forms.html
HTML
1<!-- Every input needs a label -->
2<label for="email">Email Address</label>
3<input type="email" id="email" name="email"
4 required autocomplete="email"
5 aria-describedby="email-hint" />
6
7<p id="email-hint" class="hint">
8 We'll never share your email.
9</p>
10
11<!-- Required fields -->
12<label for="name">Full Name <span aria-hidden="true">*</span></label>
13<input type="text" id="name" name="name" required
14 aria-required="true" />
15
16<!-- Error state with validation -->
17<label for="password">Password</label>
18<input
19 type="password"
20 id="password"
21 name="password"
22 required
23 minlength="8"
24 aria-invalid="true"
25 aria-describedby="password-error"
26/>
27<span id="password-error" role="alert">
28 Password must be at least 8 characters.
29</span>
30
31<!-- Grouped form controls -->
32<fieldset>
33 <legend>Shipping Address</legend>
34 <label for="street">Street</label>
35 <input type="text" id="street" name="street" />
36 <label for="city">City</label>
37 <input type="text" id="city" name="city" />
38</fieldset>
39
40<!-- Custom checkbox with proper pattern -->
41<label class="checkbox-label">
42 <input type="checkbox" checked />
43 <span>I agree to the terms and conditions</span>
44</label>
preview

info

Use aria-describedby to associate hint text with an input. Screen readers announce the description after the label. Use aria-invalid="true" on inputs with validation errors, and update it to false when the error is resolved.
Dynamic Content

Dynamic content updates — notifications, chat messages, loading states, form errors — must be announced to screen reader users. The aria-live region is the mechanism for this.

AttributeValueBehavior
aria-livepoliteAnnounces when idle (for most updates)
aria-liveassertiveInterrupts current speech (use sparingly)
rolealertEquivalent to aria-live="assertive"
rolestatusEquivalent to aria-live="polite"
aria-atomictrueAnnounce entire region, not just changed parts
dynamic.html
HTML
1<!-- Toast notification — announces content -->
2<div role="alert" aria-live="assertive" class="toast">
3 Your changes have been saved.
4</div>
5
6<!-- Chat messages — polite updates -->
7<div
8 aria-live="polite"
9 aria-atomic="false"
10 aria-relevant="additions text"
11 class="chat-messages"
12>
13 <!-- new messages appear here -->
14</div>
15
16<!-- Loading spinner with aria-live -->
17<div role="status" aria-live="polite">
18 <span class="spinner" aria-hidden="true"></span>
19 Loading search results...
20</div>
21
22<!-- JavaScript-driven announcements -->
23<script>
24function announce(message) {
25 const announcer = document.getElementById("announcer");
26 announcer.textContent = ""; // Clear first
27 // Use setTimeout to ensure change is detected
28 setTimeout(() => {
29 announcer.textContent = message;
30 }, 50);
31}
32</script>
33<div id="announcer" aria-live="polite" aria-atomic="true"
34 class="sr-only">
35</div>
🔥

pro tip

For the aria-live announcer pattern, always clear and re-set the text content with a small delay. Screen readers need to detect a content change to trigger an announcement. Using role="alert" is the simplest way for error messages — it implicitly has aria-live="assertive".
Testing

Accessibility testing combines automated tools with manual testing. Automated tools catch roughly 30% of all accessibility issues — the remaining 70% require human judgment.

Automated Tools

ToolTypeBest For
axe DevToolsBrowser extensionComprehensive automated audits
WAVEBrowser extensionVisual overlay of issues on the page
LighthouseChrome DevToolsPerformance + accessibility score
Pa11yCLI toolCI/CD pipeline integration
Accessibility InsightsDesktop app + extensionGuided manual testing workflows

Manual Testing Checklist

testing-checklist.txt
TEXT
1MANUAL ACCESSIBILITY TESTING CHECKLIST
2
3[ ] Keyboard Navigation
4 [ ] Tab through all interactive elements
5 [ ] All elements receive visible focus
6 [ ] Tab order follows visual order (no positive tabindex)
7 [ ] Enter/Space activates buttons and links
8 [ ] Arrow keys work for tabs, lists, menus
9 [ ] Escape closes dialogs and menus
10 [ ] No keyboard traps (focus gets stuck)
11
12[ ] Screen Reader
13 [ ] All images have appropriate alt text
14 [ ] Headings form a logical hierarchy (no skips)
15 [ ] Links make sense out of context
16 [ ] Forms have labels on all controls
17 [ ] Dynamic content is announced
18 [ ] ARIA landmarks are present
19 [ ] Custom widgets announce state changes
20
21[ ] Zoom & Scaling
22 [ ] Page is usable at 200% zoom
23 [ ] No horizontal scroll at 400% zoom
24 [ ] Text reflows without truncation
25
26[ ] Color & Contrast
27 [ ] All text meets 4.5:1 ratio (AA)
28 [ ] Information not conveyed by color alone
29 [ ] Focus indicators have sufficient contrast
30 [ ] Error states visible without relying on color
31
32[ ] Forms
33 [ ] All inputs have associated labels
34 [ ] Error messages linked via aria-describedby
35 [ ] Required fields indicated visually and programmatically
36 [ ] Autocomplete attributes on common fields

info

Testing with a real screen reader is irreplaceable. Try navigating your page with NVDA (Windows, free) or VoiceOver (macOS, built-in). Turn off your monitor and try to complete your primary user flow using only speech output. This will reveal issues no automated tool can find.
Best Practices Checklist
Use semantic HTML elements — this is your first and best line of defense
Add a lang attribute to the <html> element
Provide alt text for every image (informative) or alt="" (decorative)
Ensure all interactive elements are keyboard accessible
Maintain a logical heading hierarchy (h1 → h2 → h3, never skip levels)
Use ARIA landmarks to define page regions (header, nav, main, footer)
Associate every form input with a <label>
Use aria-live regions for dynamic content updates
Ensure color contrast meets WCAG AA (4.5:1 normal text, 3:1 large text)
Never convey information through color alone
Provide visible focus indicators (focus-visible)
Include a skip link as the first focusable element
Test with keyboard only (no mouse)
Test with a screen reader (VoiceOver, NVDA, JAWS)
Use automated tools (axe, Lighthouse) in CI/CD pipeline
Write accessibility tests with jest-axe or similar libraries
Ensure touch targets are at least 44×44px for mobile
Provide captions and transcripts for media content
Use progressive enhancement — core functionality should work without JavaScript
Educate your team — accessibility is everyone's responsibility

best practice

Accessibility is not a checklist — it is a mindset. Building accessible experiences means thinking about diverse users from the start. When accessibility is baked into the design and development process, it costs less, produces better UX for everyone, and creates a more inclusive web. Start small, test often, and never stop learning.
$Blueprint — Engineering Documentation·Section ID: HTML-A11Y·Revision: 1.0