♿ Accessibility Considerations
Create inclusive WordPress themes for all users
Master WCAG guidelines, ARIA attributes, and accessibility best practices
Learning Objectives
- Understand WCAG 2.1 guidelines and levels
- Implement proper semantic HTML
- Master ARIA attributes and roles
- Create keyboard-navigable interfaces
- Ensure proper color contrast
- Support screen readers effectively
- Handle focus management
- Test accessibility compliance
Understanding Web Accessibility
Web accessibility ensures that websites are usable by everyone, including people with disabilities. This includes users with visual, auditory, motor, and cognitive impairments.
Why Accessibility Matters
- Legal Requirements: Many countries require accessible websites by law (ADA, AODA, EU Directive)
- Larger Audience: 15% of the world's population has some form of disability
- Better SEO: Accessible sites rank better in search engines
- Improved Usability: Benefits all users, not just those with disabilities
- Business Ethics: The right thing to do for inclusivity
WCAG 2.1 Principles
The Web Content Accessibility Guidelines (WCAG) are organized around four fundamental principles:
Perceivable
Information must be presentable in ways users can perceive
- Text alternatives
- Captions and transcripts
- Sufficient contrast
- Resizable text
Operable
Interface components must be operable
- Keyboard accessible
- No seizure triggers
- Sufficient time
- Clear navigation
Understandable
Information and UI operation must be understandable
- Readable text
- Predictable functionality
- Input assistance
- Error identification
Robust
Content must be robust enough for various assistive technologies
- Valid HTML
- Name, role, value
- Status messages
- Compatible markup
Semantic HTML Structure
Proper Semantic Markup
<!-- header.php - Semantic header structure -->
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<a class="screen-reader-text skip-link" href="#main">
<?php esc_html_e( 'Skip to content', 'my-theme' ); ?>
</a>
<div id="page" class="site">
<header id="masthead" class="site-header" role="banner">
<div class="site-branding">
<?php if ( is_front_page() && is_home() ) : ?>
<h1 class="site-title">
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home">
<?php bloginfo( 'name' ); ?>
</a>
</h1>
<?php else : ?>
<p class="site-title">
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home">
<?php bloginfo( 'name' ); ?>
</a>
</p>
<?php endif; ?>
<?php
$description = get_bloginfo( 'description', 'display' );
if ( $description || is_customize_preview() ) :
?>
<p class="site-description"><?php echo $description; ?></p>
<?php endif; ?>
</div>
<nav id="site-navigation" class="main-navigation" role="navigation"
aria-label="<?php esc_attr_e( 'Primary Menu', 'my-theme' ); ?>">
<button class="menu-toggle" aria-controls="primary-menu"
aria-expanded="false" aria-label="<?php esc_attr_e( 'Menu', 'my-theme' ); ?>">
<span class="menu-toggle-icon">
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
</span>
<span class="menu-toggle-text"><?php esc_html_e( 'Menu', 'my-theme' ); ?></span>
</button>
<?php
wp_nav_menu( array(
'theme_location' => 'primary',
'menu_id' => 'primary-menu',
'container' => 'div',
'container_class' => 'menu-container',
'menu_class' => 'nav-menu',
'fallback_cb' => false,
'depth' => 2,
'walker' => new Accessible_Walker_Nav_Menu(),
) );
?>
</nav>
</header>
<main id="main" class="site-main" role="main">
Accessible Content Structure
<!-- single.php - Accessible post structure -->
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>
itemscope itemtype="https://schema.org/BlogPosting">
<header class="entry-header">
<?php the_title( '<h1 class="entry-title" itemprop="headline">', '</h1>' ); ?>
<div class="entry-meta">
<span class="posted-on">
<span class="screen-reader-text">
<?php esc_html_e( 'Posted on', 'my-theme' ); ?>
</span>
<time class="entry-date published" datetime="<?php echo esc_attr( get_the_date( 'c' ) ); ?>"
itemprop="datePublished">
<?php echo get_the_date(); ?>
</time>
</span>
<span class="byline">
<span class="screen-reader-text">
<?php esc_html_e( 'by', 'my-theme' ); ?>
</span>
<span class="author vcard" itemprop="author">
<a href="<?php echo esc_url( get_author_posts_url( get_the_author_meta( 'ID' ) ) ); ?>">
<?php the_author(); ?>
</a>
</span>
</span>
</div>
</header>
<div class="entry-content" itemprop="articleBody">
<?php
the_content();
wp_link_pages( array(
'before' => '<div class="page-links"><span class="page-links-title">' .
__( 'Pages:', 'my-theme' ) . '</span>',
'after' => '</div>',
'link_before' => '<span class="page-number">',
'link_after' => '</span>',
) );
?>
</div>
<footer class="entry-footer">
<?php
$categories_list = get_the_category_list( ', ' );
if ( $categories_list ) {
printf(
'<span class="cat-links"><span class="screen-reader-text">%1$s</span>%2$s</span>',
esc_html__( 'Categories:', 'my-theme' ),
$categories_list
);
}
$tags_list = get_the_tag_list( '', ', ' );
if ( $tags_list ) {
printf(
'<span class="tags-links"><span class="screen-reader-text">%1$s</span>%2$s</span>',
esc_html__( 'Tags:', 'my-theme' ),
$tags_list
);
}
?>
</footer>
</article>
ARIA Attributes and Roles
| ARIA Attribute | Purpose | Example Usage |
|---|---|---|
role |
Defines element's purpose | <nav role="navigation"> |
aria-label |
Provides accessible name | <button aria-label="Close dialog"> |
aria-labelledby |
References labeling element | <section aria-labelledby="heading-id"> |
aria-describedby |
References description | <input aria-describedby="help-text"> |
aria-expanded |
Indicates expanded state | <button aria-expanded="false"> |
aria-hidden |
Hides decorative elements | <span aria-hidden="true">👁️</span> |
aria-live |
Announces dynamic changes | <div aria-live="polite"> |
aria-current |
Indicates current item | <a aria-current="page"> |
ARIA Implementation Examples
<!-- Accessible Navigation Menu -->
<nav class="site-nav" role="navigation" aria-label="Main navigation">
<ul class="nav-menu" role="menubar">
<li role="none">
<a href="/" role="menuitem" aria-current="page">Home</a>
</li>
<li class="has-submenu" role="none">
<button role="menuitem" aria-haspopup="true" aria-expanded="false"
aria-controls="submenu-services">
Services
<span class="submenu-icon" aria-hidden="true">▼</span>
</button>
<ul id="submenu-services" class="submenu" role="menu" aria-label="Services submenu">
<li role="none">
<a href="/web-design" role="menuitem">Web Design</a>
</li>
<li role="none">
<a href="/development" role="menuitem">Development</a>
</li>
</ul>
</li>
</ul>
</nav>
<!-- Accessible Modal Dialog -->
<div class="modal" role="dialog" aria-modal="true"
aria-labelledby="modal-title" aria-describedby="modal-description">
<div class="modal-content">
<h2 id="modal-title">Subscribe to Newsletter</h2>
<p id="modal-description">Get weekly updates delivered to your inbox.</p>
<form>
<label for="email">Email Address</label>
<input type="email" id="email" required
aria-required="true" aria-describedby="email-error">
<span id="email-error" class="error" role="alert" aria-live="polite"></span>
<button type="submit">Subscribe</button>
<button type="button" class="modal-close" aria-label="Close dialog">×</button>
</form>
</div>
</div>
<!-- Live Region for Dynamic Updates -->
<div class="search-results" aria-live="polite" aria-atomic="true">
<p class="results-count">
<span class="screen-reader-text">Search results:</span>
<span class="count">0</span> results found
</p>
</div>
<!-- Accessible Tabs -->
<div class="tabs">
<div class="tab-list" role="tablist" aria-label="Content sections">
<button role="tab" aria-selected="true" aria-controls="panel-1"
id="tab-1" tabindex="0">Tab 1</button>
<button role="tab" aria-selected="false" aria-controls="panel-2"
id="tab-2" tabindex="-1">Tab 2</button>
<button role="tab" aria-selected="false" aria-controls="panel-3"
id="tab-3" tabindex="-1">Tab 3</button>
</div>
<div id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1">
<!-- Tab 1 content -->
</div>
<div id="panel-2" role="tabpanel" tabindex="0" aria-labelledby="tab-2" hidden>
<!-- Tab 2 content -->
</div>
<div id="panel-3" role="tabpanel" tabindex="0" aria-labelledby="tab-3" hidden>
<!-- Tab 3 content -->
</div>
</div>
Keyboard Navigation
Implementing Keyboard Support
// keyboard-navigation.js
class KeyboardNavigation {
constructor() {
this.initMenuKeyboardNav();
this.initModalKeyboardNav();
this.initTabsKeyboardNav();
this.initSkipLinks();
}
initMenuKeyboardNav() {
const menu = document.querySelector('.main-navigation');
if (!menu) return;
const menuItems = menu.querySelectorAll('a, button');
menuItems.forEach((item, index) => {
item.addEventListener('keydown', (e) => {
let nextIndex;
switch(e.key) {
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
nextIndex = (index + 1) % menuItems.length;
menuItems[nextIndex].focus();
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
nextIndex = (index - 1 + menuItems.length) % menuItems.length;
menuItems[nextIndex].focus();
break;
case 'Home':
e.preventDefault();
menuItems[0].focus();
break;
case 'End':
e.preventDefault();
menuItems[menuItems.length - 1].focus();
break;
case 'Escape':
if (item.getAttribute('aria-expanded') === 'true') {
this.closeSubmenu(item);
}
break;
}
});
});
}
initModalKeyboardNav() {
const modals = document.querySelectorAll('[role="dialog"]');
modals.forEach(modal => {
// Trap focus within modal
const focusableElements = modal.querySelectorAll(
'a[href], button, textarea, input[type="text"], input[type="radio"], ' +
'input[type="checkbox"], input[type="email"], select, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey) { // Shift + Tab
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
}
} else { // Tab
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
if (e.key === 'Escape') {
this.closeModal(modal);
}
});
});
}
initTabsKeyboardNav() {
const tabLists = document.querySelectorAll('[role="tablist"]');
tabLists.forEach(tabList => {
const tabs = tabList.querySelectorAll('[role="tab"]');
tabs.forEach((tab, index) => {
tab.addEventListener('keydown', (e) => {
let newIndex;
switch(e.key) {
case 'ArrowRight':
newIndex = (index + 1) % tabs.length;
this.activateTab(tabs[newIndex], tabs);
break;
case 'ArrowLeft':
newIndex = (index - 1 + tabs.length) % tabs.length;
this.activateTab(tabs[newIndex], tabs);
break;
case 'Home':
this.activateTab(tabs[0], tabs);
break;
case 'End':
this.activateTab(tabs[tabs.length - 1], tabs);
break;
}
});
});
});
}
initSkipLinks() {
// Ensure skip links are focusable
const skipLinks = document.querySelectorAll('.skip-link');
skipLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const target = document.querySelector(link.getAttribute('href'));
if (target) {
target.setAttribute('tabindex', '-1');
target.focus();
}
});
});
}
activateTab(newTab, allTabs) {
// Deactivate all tabs
allTabs.forEach(tab => {
tab.setAttribute('aria-selected', 'false');
tab.setAttribute('tabindex', '-1');
const panel = document.getElementById(tab.getAttribute('aria-controls'));
if (panel) panel.hidden = true;
});
// Activate new tab
newTab.setAttribute('aria-selected', 'true');
newTab.setAttribute('tabindex', '0');
newTab.focus();
const panel = document.getElementById(newTab.getAttribute('aria-controls'));
if (panel) panel.hidden = false;
}
closeSubmenu(button) {
button.setAttribute('aria-expanded', 'false');
const submenu = document.getElementById(button.getAttribute('aria-controls'));
if (submenu) submenu.hidden = true;
}
closeModal(modal) {
modal.hidden = true;
// Return focus to trigger element
const trigger = document.querySelector(`[aria-controls="${modal.id}"]`);
if (trigger) trigger.focus();
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
new KeyboardNavigation();
});
Accessibility Testing Tools
axe DevTools
Browser extension for automated testing
- Finds WCAG violations
- Provides fix suggestions
- Chrome/Firefox/Edge
WAVE
Web Accessibility Evaluation Tool
- Visual feedback
- Detailed reports
- Browser extension
NVDA
Free screen reader for Windows
- Test with real AT
- Speech viewer
- Free and open source
Lighthouse
Chrome DevTools built-in audit
- Accessibility score
- Performance metrics
- Best practices
Pa11y
Command line testing tool
- Automated testing
- CI/CD integration
- Multiple standards
Contrast Checkers
Color contrast validation
- WebAIM Contrast Checker
- Stark (Figma/Sketch)
- Chrome DevTools
WordPress Theme Accessibility Checklist
- Skip links to main content
- Keyboard navigation for all interactive elements
- Focus indicators visible and clear
- Proper heading hierarchy (h1-h6)
- All images have alt text
- Form labels properly associated
- Error messages clearly identified
- Color contrast meets WCAG AA (4.5:1 normal, 3:1 large text)
- Text resizable to 200% without horizontal scroll
- ARIA landmarks used appropriately
- Buttons and links distinguishable
- Touch targets at least 44x44 pixels
- Media has captions/transcripts
- No content relies solely on color
- Animations can be paused/stopped
- No automatic media playback
- Tables have proper headers
- Page language declared
- Consistent navigation
- Tested with screen reader
Best Practices
Accessibility Best Practices
- Start with semantic HTML: Use proper elements before adding ARIA
- Test early and often: Don't wait until the end
- Use real assistive technology: Test with actual screen readers
- Involve users with disabilities: Get real feedback
- Progressive enhancement: Build on a solid foundation
- Document accessibility features: Help users understand
- Keep learning: Accessibility standards evolve
- Automate testing: Include in CI/CD pipeline
- Design inclusively: Consider accessibility from the start
- Provide alternatives: Multiple ways to accomplish tasks