Best Practices: Use semantic elements instead of divs where possible. Maintain logical heading
hierarchy (don't skip levels). One <main> per page. Label multiple navs with aria-label. Use <time>
for dates with datetime attribute. Provide skip-to-content links for keyboard users.
2. ARIA Roles, States, and Properties
ARIA Category
Purpose
Examples
Roles
Define element type/purpose
button, dialog, tablist, alert
States
Current condition (can change)
aria-checked, aria-expanded, aria-pressed
Properties
Characteristics (rarely change)
aria-label, aria-labelledby, aria-describedby
Common ARIA Role
Use When
Required ARIA Attributes
role="button"
Clickable non-button element
tabindex="0", keyboard handlers
role="dialog"
Modal or dialog window
aria-labelledby or aria-label
role="alert"
Important, time-sensitive message
None (auto-announced)
role="tablist"
Tab navigation component
With role="tab" and role="tabpanel"
role="navigation"
Navigation links Use <nav>
aria-label (if multiple navs)
role="search"
Search form
None
role="status"
Status message (low priority)
None (politely announced)
role="progressbar"
Progress indicator
aria-valuenow, aria-valuemin, aria-valuemax
ARIA Property
Purpose
Example
aria-label
Provide accessible name
aria-label="Close dialog"
aria-labelledby
Reference element ID for label
aria-labelledby="dialog-title"
aria-describedby
Reference element ID for description
aria-describedby="hint-text"
aria-hidden
Hide from screen readers
aria-hidden="true" (decorative icons)
aria-live
Announce dynamic content
aria-live="polite" or "assertive"
aria-expanded
Collapsible element state
aria-expanded="false"
aria-pressed
Toggle button state
aria-pressed="true"
aria-checked
Checkbox/radio state
aria-checked="true"
aria-selected
Selection state (tabs, options)
aria-selected="true"
aria-disabled
Disabled state (still focusable)
aria-disabled="true"
aria-current
Current item in set
aria-current="page" or "step"
Example: Custom button with ARIA
<!-- Native button (preferred) --><button type="button">Click Me</button><!-- Custom button with ARIA (if native not possible) --><div role="button" tabindex="0" aria-label="Close dialog" onclick="closeDialog()" onkeydown="handleKeyPress(event)"> <span aria-hidden="true">×</span></div><script>function handleKeyPress(e) { // Space or Enter activates button if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); closeDialog(); }}</script>
<!-- Polite announcement (waits for pause) --><div role="status" aria-live="polite" aria-atomic="true"> <!-- Content updated via JavaScript --> <p>5 new messages</p></div><!-- Assertive announcement (interrupts) --><div role="alert" aria-live="assertive"> <!-- Urgent notifications --> <p>Error: Connection lost</p></div><!-- Loading state --><div role="status" aria-live="polite" aria-busy="true" aria-label="Loading content"> <span class="spinner"></span> Loading...</div><script>// Update live regionfunction updateStatus(message) { const status = document.querySelector('[role="status"]'); status.textContent = message; // Screen reader announces: "5 new messages"}// Show errorfunction showError(error) { const alert = document.querySelector('[role="alert"]'); alert.textContent = error; // Screen reader immediately announces error}</script>
ARIA Rules: First rule: Don't use ARIA if native HTML element exists (use <button> not
<div role="button">). Second rule: Don't change native semantics (don't add role to <h1>). ARIA
doesn't provide behavior - you must add keyboard handlers and focus management. Test with actual screen readers.
<div role="dialog" aria-labelledby="dialog-title" aria-modal="true" class="modal"> <h2 id="dialog-title">Confirm Action</h2> <p>Are you sure you want to continue?</p> <button id="confirm-btn">Confirm</button> <button id="cancel-btn">Cancel</button></div><script>class ModalDialog { constructor(element) { this.modal = element; this.focusableElements = this.modal.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); this.firstFocusable = this.focusableElements[0]; this.lastFocusable = this.focusableElements[this.focusableElements.length - 1]; this.previousFocus = null; } open() { // Store current focus this.previousFocus = document.activeElement; // Show modal this.modal.hidden = false; // Move focus to first element this.firstFocusable.focus(); // Add event listeners this.modal.addEventListener('keydown', this.handleKeyDown.bind(this)); } close() { // Hide modal this.modal.hidden = true; // Restore focus if (this.previousFocus) { this.previousFocus.focus(); } // Remove listeners this.modal.removeEventListener('keydown', this.handleKeyDown); } handleKeyDown(e) { // Close on Escape if (e.key === 'Escape') { this.close(); return; } // Focus trap on Tab if (e.key === 'Tab') { if (e.shiftKey) { // Shift + Tab if (document.activeElement === this.firstFocusable) { e.preventDefault(); this.lastFocusable.focus(); } } else { // Tab if (document.activeElement === this.lastFocusable) { e.preventDefault(); this.firstFocusable.focus(); } } } }}// Usageconst modal = new ModalDialog(document.querySelector('.modal'));document.getElementById('open-modal-btn').addEventListener('click', () => { modal.open();});document.getElementById('cancel-btn').addEventListener('click', () => { modal.close();});</script>
Example: Roving tabindex for radio group
<div role="radiogroup" aria-labelledby="group-label"> <p id="group-label">Choose your favorite color:</p> <div role="radio" aria-checked="true" tabindex="0"> Red </div> <div role="radio" aria-checked="false" tabindex="-1"> Green </div> <div role="radio" aria-checked="false" tabindex="-1"> Blue </div></div><script>const radioGroup = document.querySelector('[role="radiogroup"]');const radios = Array.from(radioGroup.querySelectorAll('[role="radio"]'));radioGroup.addEventListener('keydown', (e) => { const current = document.activeElement; const currentIndex = radios.indexOf(current); let nextIndex; switch(e.key) { case 'ArrowDown': case 'ArrowRight': e.preventDefault(); nextIndex = (currentIndex + 1) % radios.length; break; case 'ArrowUp': case 'ArrowLeft': e.preventDefault(); nextIndex = (currentIndex - 1 + radios.length) % radios.length; break; case ' ': case 'Enter': e.preventDefault(); selectRadio(current); return; default: return; } // Update tabindex and focus radios[currentIndex].tabIndex = -1; radios[nextIndex].tabIndex = 0; radios[nextIndex].focus();});function selectRadio(radio) { radios.forEach(r => r.setAttribute('aria-checked', 'false')); radio.setAttribute('aria-checked', 'true');}// Click handlerradios.forEach(radio => { radio.addEventListener('click', () => selectRadio(radio));});</script>
Focus Best Practices: Always show visible focus indicator (:focus styles). Never use outline:
none without alternative. Maintain logical tab order (matches visual flow). Use tabindex="0" for custom
controls, never positive values. Trap focus in modals. Restore focus when closing overlays. Test with keyboard
only (no mouse).
4. Alternative Text and Media Descriptions
Element
Attribute
Purpose
Example
<img>
alt
Describes image content
alt="Woman using laptop"
<img> (decorative)
alt=""
Empty for decorative images
alt=""
<video>
Text tracks
Captions and descriptions
<track kind="captions">
<audio>
Fallback content
Transcript link
<a href="transcript.txt">
<figure>
<figcaption>
Extended description
<figcaption>Chart showing...</figcaption>
SVG
<title>, <desc>
SVG accessibility
<title>Icon name</title>
Icon fonts
aria-label
Describe icon purpose
aria-label="Search"
Complex images
aria-describedby
Link to detailed description
aria-describedby="chart-desc"
Example: Image Alt Text Guidelines
<!-- Informative image --><img src="graph.png" alt="Sales increased 25% in Q4 2023" /><!-- Functional image (link/button) --><a href="home.html"> <img src="logo.png" alt="Company Name Home" /></a><!-- Decorative image --><img src="divider.png" alt="" role="presentation" /><!-- Complex image with extended description --><figure> <img src="chart.png" alt="Revenue chart 2020-2023" aria-describedby="chart-details" /> <figcaption id="chart-details"> Bar chart showing revenue growth from $2M in 2020 to $8M in 2023, with steady increase each year. </figcaption></figure><!-- Image in text flow --><p> Our CEO <img src="ceo.jpg" alt="Jane Smith" /> announced the new initiative.</p>
Example: Video with Captions and Audio Description
<!-- SVG with title and description --><svg role="img" aria-labelledby="icon-title icon-desc"> <title id="icon-title">Success</title> <desc id="icon-desc">Green checkmark indicating success</desc> <path d="M10 15 L5 10 L7 8 L10 11 L17 4 L19 6 Z" /></svg><!-- Icon font with aria-label --><button> <i class="icon-trash" aria-hidden="true"></i> <span class="sr-only">Delete item</span></button><!-- Or using aria-label directly --><button aria-label="Close dialog"> <span class="icon-close" aria-hidden="true">×</span></button><!-- Decorative SVG --><svg aria-hidden="true" focusable="false"> <path d="..." /></svg>
Alt Text Writing Guidelines: Be specific and concise (under 150 characters). Describe the
information, not the image itself ("graph showing sales trends" not "graph image"). Include text visible in the
image. Don't start with "image of" or "picture of". For functional images (links/buttons), describe the
action/destination. Use alt="" for purely decorative images. Provide extended descriptions for complex images
(charts, diagrams) using figcaption or aria-describedby.
5. Form Accessibility and Error Handling
Technique
Implementation
Purpose
Label association
<label for="id">
Connect labels to inputs
Required fields
required + aria-required="true"
Indicate mandatory fields
Error messages
aria-invalid + aria-describedby
Associate errors with fields
Field instructions
aria-describedby
Provide helpful hints
Fieldset grouping
<fieldset> + <legend>
Group related inputs
Error summary
role="alert"
Announce validation errors
Input patterns
pattern + title
Define expected format
Autocomplete
autocomplete attribute
Help users fill forms faster
Example: Accessible Form with Validation
<form novalidate> <!-- Text input with label and hint --> <div class="form-group"> <label for="username"> Username <span class="required">*</span> </label> <input type="text" id="username" name="username" required aria-required="true" aria-describedby="username-hint" autocomplete="username" /> <small id="username-hint"> Must be 3-20 characters, letters and numbers only </small> </div> <!-- Email with error message --> <div class="form-group"> <label for="email"> Email <span class="required">*</span> </label> <input type="email" id="email" name="email" required aria-required="true" aria-invalid="true" aria-describedby="email-error" autocomplete="email" /> <span id="email-error" class="error" role="alert"> Please enter a valid email address </span> </div> <!-- Radio group --> <fieldset> <legend>Account type <span class="required">*</span></legend> <div> <input type="radio" id="personal" name="account" value="personal" required /> <label for="personal">Personal</label> </div> <div> <input type="radio" id="business" name="account" value="business" /> <label for="business">Business</label> </div> </fieldset> <!-- Checkbox --> <div class="form-group"> <input type="checkbox" id="terms" name="terms" required aria-required="true" aria-describedby="terms-error" /> <label for="terms"> I agree to the <a href="terms.html">terms</a> </label> <span id="terms-error" class="error" role="alert" hidden> You must agree to the terms </span> </div> <button type="submit">Create Account</button></form>
Example: Error Summary and Focus Management
// Error summary at top of form<div id="error-summary" class="error-summary" role="alert" tabindex="-1" hidden> <h2>Please correct the following errors:</h2> <ul> <li><a href="#username">Username is required</a></li> <li><a href="#email">Email is invalid</a></li> <li><a href="#password">Password is too short</a></li> </ul></div><form id="signup-form"> <!-- Form fields... --></form><script>// Validation and error handlingconst form = document.getElementById('signup-form');const errorSummary = document.getElementById('error-summary');form.addEventListener('submit', function(e) { e.preventDefault(); const errors = validateForm(); if (errors.length > 0) { // Show error summary errorSummary.hidden = false; // Populate error list const errorList = errorSummary.querySelector('ul'); errorList.innerHTML = errors.map(error => `<li><a href="#${error.field}">${error.message}</a></li>` ).join(''); // Focus on error summary errorSummary.focus(); // Mark invalid fields errors.forEach(error => { const field = document.getElementById(error.field); field.setAttribute('aria-invalid', 'true'); // Show field-level error const errorMsg = document.getElementById(`${error.field}-error`); if (errorMsg) { errorMsg.hidden = false; } }); } else { // Submit form this.submit(); }});// Clear error on inputform.addEventListener('input', function(e) { if (e.target.matches('input, textarea, select')) { e.target.setAttribute('aria-invalid', 'false'); const errorMsg = document.getElementById(`${e.target.id}-error`); if (errorMsg) { errorMsg.hidden = true; } }});</script>
Form Accessibility Best Practices: Always use explicit <label> elements with for
attribute. Mark required fields with required attribute AND visual indicator (*). Use aria-required="true" for
custom controls. Provide clear, specific error messages associated with fields using aria-describedby. Mark
invalid fields with aria-invalid="true". Show errors both at field level and in summary. Focus on error summary
or first invalid field on submit. Use fieldset/legend for radio/checkbox groups. Provide instructions before the
field, not just in placeholder. Use autocomplete attributes to help users. Test with screen reader and keyboard
only.
6. Color Contrast and Visual Accessibility
Standard
Contrast Ratio
Requirement
WCAG 2.1 Level AA
4.5:1
Normal text (under 18pt or 14pt bold)
WCAG 2.1 Level AA
3:1
Large text (18pt+ or 14pt+ bold)
WCAG 2.1 Level AAA
7:1
Normal text (enhanced)
WCAG 2.1 Level AAA
4.5:1
Large text (enhanced)
UI Components
3:1
Graphical objects, form controls
Focus indicators
3:1
Against adjacent colors
Technique
Purpose
Implementation
Color + pattern
Don't rely on color alone
Use icons, shapes, patterns, labels
Visible focus
Show keyboard focus
:focus and :focus-visible styles
Link identification
Distinguish from text
Underline, bold, or 3:1 contrast + indicator
Text over images
Ensure readability
Overlay, shadows, sufficient contrast
Resize text
Support 200% zoom
Relative units (rem, em), no text in images
Reflow
320px width support
Responsive design, avoid horizontal scroll
Animation control
Reduce motion
prefers-reduced-motion media query
Example: Color Contrast Examples
<!-- Good contrast (AA compliant) --><p style="color: #333; background: #fff;"> Dark gray on white (11:1 ratio) ✓</p><button style="color: #fff; background: #0066cc;"> White on blue (6.5:1 ratio) ✓</button><!-- Insufficient contrast (fails AA) --><p style="color: #999; background: #fff;"> Light gray on white (2.8:1 ratio) ✗</p><a href="#" style="color: #87ceeb; background: #fff;"> Sky blue on white (2.4:1 ratio) ✗</a><!-- Large text can use lower contrast --><h1 style="font-size: 24pt; color: #777; background: #fff;"> Large heading (3.4:1 ratio) ✓ (AA for large text)</h1>
Example: Not Relying on Color Alone
<!-- Bad: Color only --><p> Required fields are in <span style="color: red;">red</span>.</p><!-- Good: Color + symbol --><label for="name"> Name <span class="required" aria-label="required">*</span></label><!-- Bad: Color-coded status --><div class="status-green">Active</div><div class="status-red">Inactive</div><!-- Good: Color + icon + text --><div class="status status-active"> <svg aria-hidden="true"><!-- checkmark icon --></svg> <span>Active</span></div><div class="status status-inactive"> <svg aria-hidden="true"><!-- x icon --></svg> <span>Inactive</span></div><!-- Charts: use patterns in addition to colors --><svg role="img" aria-label="Sales by region"> <!-- Use different fill patterns for each segment --> <rect fill="url(#pattern-stripes)" /> <rect fill="url(#pattern-dots)" /> <defs> <pattern id="pattern-stripes">...</pattern> <pattern id="pattern-dots">...</pattern> </defs></svg>
Visual Accessibility Checklist: Ensure 4.5:1 contrast for normal text, 3:1 for large text and
UI components. Test with contrast checker tools. Never use color alone to convey information—add icons,
patterns, or text labels. Provide visible focus indicators (minimum 3:1 contrast). Make links distinguishable
from regular text (underline or sufficient contrast difference). Support text resize up to 200% without loss of
content or functionality. Ensure content reflows at 320px width without horizontal scrolling. Respect
prefers-reduced-motion for animations. Avoid text in images; use real text with CSS styling. Test with different
zoom levels, color blindness simulators, and high contrast mode.
Section 14: Key Takeaways
Semantic HTML: Use proper semantic elements (header, nav, main, article, aside, footer)
to create document structure that screen readers can navigate effectively
ARIA: Enhance accessibility with ARIA roles, states, and properties when native HTML
semantics are insufficient; use aria-label, aria-labelledby, aria-describedby, aria-live
Keyboard Navigation: Ensure all interactive elements are keyboard accessible with Tab,
Enter, Space, Arrow keys; manage focus properly with tabindex, skip links, and focus traps
Alternative Text: Provide meaningful alt text for images (descriptive, concise, under 150
chars); use alt="" for decorative images; provide captions and transcripts for multimedia
Form Accessibility: Associate labels with inputs using for attribute; mark required
fields; show inline validation errors with aria-invalid and aria-describedby; use autocomplete attributes
Color Contrast: Maintain minimum 4.5:1 ratio for normal text, 3:1 for large text and UI
components; never rely on color alone—add icons, patterns, or text labels
Focus Indicators: Always provide visible focus styles (:focus, :focus-visible) with
minimum 3:1 contrast; never remove outlines without accessible alternatives
Motion Sensitivity: Respect prefers-reduced-motion media query to disable or reduce
animations for users with vestibular disorders
Text Scaling: Use relative units (rem, em) to support text resize up to 200%; ensure
content reflows at 320px width without horizontal scrolling
Testing: Test with keyboard only (no mouse), screen readers (NVDA, JAWS, VoiceOver),
color contrast checkers, and browser accessibility tools to verify compliance with WCAG 2.1 Level AA