HTML Accessibility (A11y)
1. Semantic HTML for Screen Readers
| Semantic Element | Purpose | Screen Reader Benefit | Replaces |
|---|---|---|---|
| <header> | Page/section header | Announces "banner" or "header" landmark | <div class="header"> |
| <nav> | Navigation section | Announces "navigation" landmark, quick access | <div class="nav"> |
| <main> | Primary content | Announces "main" landmark, skip-to-content | <div class="main"> |
| <article> | Self-contained content | Announces article boundary, easier navigation | <div class="article"> |
| <section> | Thematic content grouping | Announces section, hierarchical structure | <div class="section"> |
| <aside> | Sidebar/tangential content | Announces "complementary" landmark | <div class="sidebar"> |
| <footer> | Page/section footer | Announces "contentinfo" or "footer" landmark | <div class="footer"> |
| Heading Level | Purpose | Rules |
|---|---|---|
| <h1> | Page title/main heading | One per page (ideally), highest level |
| <h2> | Major section headings | Direct children of h1 sections |
| <h3> - <h6> | Subsection headings | Don't skip levels (h2 → h4 ❌) |
| List Type | When to Use | Screen Reader Announcement |
|---|---|---|
| <ul> | Unordered list (bullets) | "List, X items" |
| <ol> | Ordered list (numbered) | "List, X items" + item numbers |
| <dl> | Definition/description list | Announces term-definition pairs |
Example: Semantic page structure for screen readers
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Accessible Page Example</title>
</head>
<body>
<!-- Skip to main content link (hidden but accessible) -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Header landmark -->
<header>
<h1>Website Title</h1>
<p>Tagline or description</p>
</header>
<!-- Navigation landmark -->
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<!-- Main landmark (only one per page) -->
<main id="main-content">
<!-- Article 1 -->
<article>
<h2>Article Title</h2>
<p>Published on <time datetime="2025-12-22">December 22, 2025</time></p>
<section>
<h3>Introduction</h3>
<p>Article introduction...</p>
</section>
<section>
<h3>Main Content</h3>
<p>Article content...</p>
<section>
<h4>Subsection</h4>
<p>Subsection content...</p>
</section>
</section>
</article>
<!-- Article 2 -->
<article>
<h2>Another Article</h2>
<p>Content...</p>
</article>
</main>
<!-- Complementary landmark -->
<aside aria-label="Sidebar">
<section>
<h2>Related Links</h2>
<ul>
<li><a href="/related1">Related Article 1</a></li>
<li><a href="/related2">Related Article 2</a></li>
</ul>
</section>
</aside>
<!-- Contentinfo landmark -->
<footer>
<p>© 2025 Company Name. All rights reserved.</p>
<nav aria-label="Footer navigation">
<ul>
<li><a href="/privacy">Privacy Policy</a></li>
<li><a href="/terms">Terms of Service</a></li>
</ul>
</nav>
</footer>
</body>
</html>
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 (role attribute), 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>
Example: Accessible tabs with ARIA
<div class="tabs">
<!-- Tab list -->
<div role="tablist" aria-label="Content tabs">
<button
role="tab"
id="tab-1"
aria-selected="true"
aria-controls="panel-1"
tabindex="0">
Tab 1
</button>
<button
role="tab"
id="tab-2"
aria-selected="false"
aria-controls="panel-2"
tabindex="-1">
Tab 2
</button>
<button
role="tab"
id="tab-3"
aria-selected="false"
aria-controls="panel-3"
tabindex="-1">
Tab 3
</button>
</div>
<!-- Tab panels -->
<div
role="tabpanel"
id="panel-1"
aria-labelledby="tab-1"
tabindex="0">
Content for tab 1
</div>
<div
role="tabpanel"
id="panel-2"
aria-labelledby="tab-2"
hidden
tabindex="0">
Content for tab 2
</div>
<div
role="tabpanel"
id="panel-3"
aria-labelledby="tab-3"
hidden
tabindex="0">
Content for tab 3
</div>
</div>
Example: Live regions for dynamic updates
<!-- 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 region
function updateStatus(message) {
const status = document.querySelector('[role="status"]');
status.textContent = message;
// Screen reader announces: "5 new messages"
}
// Show error
function 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.
3. Keyboard Navigation (tabindex) and Focus Management
| Keyboard Action | Expected Behavior | Elements |
|---|---|---|
| Tab | Move focus to next focusable element | All interactive elements |
| Shift + Tab | Move focus to previous focusable element | All interactive elements |
| Enter | Activate element (click) | Links, buttons, form submissions |
| Space | Activate button, check checkbox | Buttons, checkboxes |
| Arrow Keys | Navigate within component | Radio groups, tabs, menus, sliders |
| Escape | Close/cancel | Dialogs, menus, dropdowns |
| Home / End | Jump to first/last item | Lists, sliders, combo boxes |
| tabindex Value | Behavior | Use Case |
|---|---|---|
| tabindex="0" | Natural tab order, focusable | Make div/span focusable (custom controls) |
| tabindex="-1" | Not in tab order, programmatically focusable | Headings for skip links, modal content |
| tabindex="1+" | Explicit order AVOID | ❌ Breaks natural order, causes confusion |
| No tabindex | Native focusable elements only | Default for button, input, a, etc. |
| Focus Pattern | Implementation | Use Case |
|---|---|---|
| Focus Trap | Cycle Tab within container | Modal dialogs, dropdowns |
| Focus Restoration | Return focus to trigger element | Close modal, dropdown, menu |
| Skip Links | Jump to main content | Bypass navigation, headers |
| Roving Tabindex | One focusable item in group, arrows move | Radio groups, toolbars, tabs |
Example: Skip to main content link
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
</style>
<!-- Visible on focus only -->
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<header>...navigation...</header>
<main id="main-content" tabindex="-1">
<h1>Page Title</h1>
<!-- Content -->
</main>
Example: Focus trap in modal dialog
<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();
}
}
}
}
}
// Usage
const 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 handler
radios.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 (alt attribute) 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
<video controls>
<source src="video.mp4" type="video/mp4" />
<source src="video.webm" type="video/webm" />
<!-- Captions (subtitles) -->
<track kind="captions" src="captions-en.vtt"
srclang="en" label="English" default />
<track kind="captions" src="captions-es.vtt"
srclang="es" label="Spanish" />
<!-- Audio descriptions for visually impaired -->
<track kind="descriptions" src="descriptions.vtt"
srclang="en" label="Audio Description" />
<!-- Fallback content -->
<p>Your browser doesn't support video.
<a href="transcript.html">Read transcript</a>
</p>
</video>
<!-- Provide transcript link -->
<p><a href="transcript.html">Full video transcript</a></p>
Example: Accessible SVG and Icon Fonts
<!-- 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 (aria-label, aria-describedby) 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 handling
const 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 input
form.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>
Example: Autocomplete Attributes
<!-- Personal information -->
<input type="text" name="name" autocomplete="name" />
<input type="text" name="fname" autocomplete="given-name" />
<input type="text" name="lname" autocomplete="family-name" />
<input type="email" name="email" autocomplete="email" />
<input type="tel" name="phone" autocomplete="tel" />
<!-- Address fields -->
<input type="text" autocomplete="street-address" />
<input type="text" autocomplete="address-line1" />
<input type="text" autocomplete="address-line2" />
<input type="text" autocomplete="address-level2" /> <!-- City -->
<input type="text" autocomplete="address-level1" /> <!-- State -->
<input type="text" autocomplete="postal-code" />
<input type="text" autocomplete="country-name" />
<!-- Authentication -->
<input type="text" autocomplete="username" />
<input type="password" autocomplete="current-password" />
<input type="password" autocomplete="new-password" />
<!-- Payment -->
<input type="text" autocomplete="cc-name" />
<input type="text" autocomplete="cc-number" />
<input type="text" autocomplete="cc-exp" />
<input type="text" autocomplete="cc-csc" />
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 (WCAG)
| 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>
Example: Focus Indicators and Reduced Motion
<style>
/* Visible focus indicator */
a:focus,
button:focus,
input:focus {
outline: 3px solid #0066cc;
outline-offset: 2px;
}
/* Enhanced focus for keyboard navigation */
:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(0, 102, 204, 0.25);
}
/* Remove outline for mouse clicks (but keep for keyboard) */
:focus:not(:focus-visible) {
outline: none;
}
/* High contrast links */
a {
color: #0066cc;
text-decoration: underline;
text-underline-offset: 2px;
}
a:hover {
color: #004499;
text-decoration-thickness: 2px;
}
/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Default smooth animations */
@media (prefers-reduced-motion: no-preference) {
.card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
}
}
/* Text resize support */
html {
font-size: 16px; /* Base size */
}
body {
font-size: 1rem; /* Relative to root */
line-height: 1.5;
}
h1 { font-size: 2rem; }
h2 { font-size: 1.5rem; }
p { font-size: 1rem; }
/* Ensure text can scale to 200% */
.container {
max-width: 1200px;
padding: 0 1rem;
}
@media (max-width: 480px) {
/* Reflow content, no horizontal scroll */
.grid {
grid-template-columns: 1fr;
}
}
</style>
Example: Text Over Images
<!-- Technique 1: Dark overlay -->
<div class="hero" style="
background-image: url('photo.jpg');
background-size: cover;
position: relative;
">
<div style="
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.6);
"></div>
<h1 style="
position: relative;
color: white;
z-index: 1;
">
Hero Title (Sufficient Contrast)
</h1>
</div>
<!-- Technique 2: Text shadow -->
<div class="banner" style="background-image: url('bg.jpg');">
<h2 style="
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8),
0 0 8px rgba(0, 0, 0, 0.6);
">
Heading with Shadow
</h2>
</div>
<!-- Technique 3: Background box -->
<div class="card" style="background-image: url('pattern.jpg');">
<p style="
background: rgba(255, 255, 255, 0.95);
padding: 1rem;
color: #333;
">
Text in semi-transparent box
</p>
</div>
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