Skip to main content

Course Progress

Loading...

♿ Accessibility Best Practices

Create inclusive WordPress themes for everyone

Master WCAG guidelines and build accessible user experiences

Learning Objectives

  • Understand WCAG 2.1 guidelines and principles
  • Implement semantic HTML structure
  • Use ARIA attributes appropriately
  • Ensure keyboard navigation works properly
  • Create accessible forms and navigation
  • Implement proper color contrast
  • Support screen readers effectively
  • Test accessibility compliance

Why Accessibility Matters

Web accessibility ensures that people with disabilities can use your website. It's not just about compliance—it's about creating inclusive experiences that benefit everyone.

WCAG 2.1 Four Principles

👁️

Perceivable

Information must be presentable in ways users can perceive

⚙️

Operable

Interface components must be operable by all users

📖

Understandable

Information and UI operation must be understandable

💪

Robust

Content must work with various assistive technologies

💡
Legal Requirements
Many countries require WCAG 2.1 Level AA compliance for public websites. In the US, ADA compliance is increasingly enforced for commercial websites.

Semantic HTML Structure

Proper Document Structure

<!-- Good: Semantic HTML -->
<header role="banner">
    <nav role="navigation" 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>
</header>

<main role="main">
    <article>
        <h1>Page Title</h1>
        <section>
            <h2>Section Heading</h2>
            <p>Content...</p>
        </section>
    </article>
    
    <aside role="complementary">
        <h2>Related Content</h2>
        <!-- Sidebar content -->
    </aside>
</main>

<footer role="contentinfo">
    <p>© 2024 Site Name</p>
</footer>

<!-- Bad: Non-semantic HTML -->
<div class="header">
    <div class="nav">
        <div class="menu-item">Home</div>
        <div class="menu-item">About</div>
    </div>
</div>

✅ DO

  • Use semantic HTML5 elements
  • Maintain proper heading hierarchy
  • Use lists for navigation
  • Include landmark roles
  • Provide skip links

❌ DON'T

  • Use divs for everything
  • Skip heading levels
  • Use tables for layout
  • Rely only on color
  • Use placeholder as label

WordPress Accessibility Implementation

Accessible Navigation Menu

<?php
/**
 * Accessible Navigation Walker
 */
class Accessible_Walker_Nav_Menu extends Walker_Nav_Menu {
    
    function start_lvl( &$output, $depth = 0, $args = null ) {
        $indent = str_repeat( "\t", $depth );
        $output .= "\n$indent<ul class=\"sub-menu\" role=\"menu\" aria-label=\"Submenu\">\n";
    }
    
    function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
        $indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
        
        $classes = empty( $item->classes ) ? array() : (array) $item->classes;
        $classes[] = 'menu-item-' . $item->ID;
        
        // Add aria-current for current page
        $aria_current = '';
        if ( in_array( 'current-menu-item', $classes ) ) {
            $aria_current = ' aria-current="page"';
        }
        
        $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
        
        $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );
        $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
        
        $output .= $indent . '<li' . $id . $class_names . ' role="menuitem">';
        
        $attributes = ! empty( $item->attr_title ) ? ' title="' . esc_attr( $item->attr_title ) .'"' : '';
        $attributes .= ! empty( $item->target ) ? ' target="' . esc_attr( $item->target ) .'"' : '';
        $attributes .= ! empty( $item->xfn ) ? ' rel="' . esc_attr( $item->xfn ) .'"' : '';
        $attributes .= ! empty( $item->url ) ? ' href="' . esc_attr( $item->url ) .'"' : '';
        $attributes .= $aria_current;
        
        // Add dropdown toggle for parent items
        if ( in_array( 'menu-item-has-children', $classes ) ) {
            $attributes .= ' aria-haspopup="true" aria-expanded="false"';
        }
        
        $item_output = $args->before;
        $item_output .= '<a'. $attributes .'>';
        $item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
        $item_output .= '</a>';
        $item_output .= $args->after;
        
        $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
    }
}

// Usage
wp_nav_menu( array(
    'theme_location' => 'primary',
    'menu_class'     => 'nav-menu',
    'container'      => 'nav',
    'container_class' => 'main-navigation',
    'container_aria_label' => 'Main navigation',
    'walker'         => new Accessible_Walker_Nav_Menu(),
) );

Skip Links

<?php
/**
 * Add skip links
 */
function mytheme_skip_links() {
    ?>
    <a class="screen-reader-text skip-link" href="#main">
        <?php esc_html_e( 'Skip to content', 'mytheme' ); ?>
    </a>
    <a class="screen-reader-text skip-link" href="#primary-nav">
        <?php esc_html_e( 'Skip to navigation', 'mytheme' ); ?>
    </a>
    <a class="screen-reader-text skip-link" href="#footer">
        <?php esc_html_e( 'Skip to footer', 'mytheme' ); ?>
    </a>
    <?php
}
add_action( 'wp_body_open', 'mytheme_skip_links' );

// CSS for skip links
.screen-reader-text {
    clip: rect(1px, 1px, 1px, 1px);
    position: absolute !important;
    height: 1px;
    width: 1px;
    overflow: hidden;
}

.screen-reader-text:focus {
    clip: auto !important;
    display: block;
    height: auto;
    left: 5px;
    top: 5px;
    width: auto;
    z-index: 100000;
    padding: 15px 20px;
    background: #000;
    color: #fff;
    text-decoration: none;
    box-shadow: 0 0 10px rgba(0,0,0,0.5);
}

ARIA Attributes

Common ARIA Attributes

<!-- Labels and Descriptions -->
<button aria-label="Close dialog">X</button>
<input type="search" aria-label="Search site" />
<div aria-labelledby="dialog-title">...</div>
<input aria-describedby="password-help" />
<span id="password-help">Must be at least 8 characters</span>

<!-- States -->
<button aria-pressed="false">Toggle</button>
<div aria-hidden="true">Decorative content</div>
<nav aria-expanded="false">Mobile menu</nav>
<li aria-current="page">Current page</li>

<!-- Relationships -->
<div role="tablist">
    <button role="tab" aria-selected="true" aria-controls="panel1">Tab 1</button>
    <button role="tab" aria-selected="false" aria-controls="panel2">Tab 2</button>
</div>
<div role="tabpanel" id="panel1" aria-labelledby="tab1">...</div>

<!-- Landmarks -->
<nav role="navigation" aria-label="Main">...</nav>
<aside role="complementary" aria-label="Related links">...</aside>
<div role="search">...</div>

Live Regions for Dynamic Content

<!-- Status messages -->
<div role="status" aria-live="polite" aria-atomic="true">
    <p>Form saved successfully</p>
</div>

<!-- Error messages -->
<div role="alert" aria-live="assertive">
    <p>Error: Invalid email address</p>
</div>

<!-- Loading states -->
<div aria-live="polite" aria-busy="true">
    <span class="spinner"></span>
    Loading content...
</div>

<!-- Search results -->
<div id="search-results" aria-live="polite" aria-relevant="additions removals">
    <h2><span id="result-count">5</span> results found</h2>
    <!-- Results list -->
</div>

<!-- JavaScript implementation -->
<script>
// Announce dynamic changes
function announceChange(message) {
    const announcement = document.createElement('div');
    announcement.setAttribute('role', 'status');
    announcement.setAttribute('aria-live', 'polite');
    announcement.className = 'screen-reader-text';
    announcement.textContent = message;
    
    document.body.appendChild(announcement);
    
    setTimeout(() => {
        document.body.removeChild(announcement);
    }, 1000);
}
</script>

Accessible Widget Patterns

<?php
/**
 * Accessible Modal Dialog
 */
function mytheme_modal_dialog() {
    ?>
    <div id="modal" 
         role="dialog" 
         aria-modal="true" 
         aria-labelledby="modal-title" 
         aria-describedby="modal-description"
         tabindex="-1">
        
        <div class="modal-content">
            <h2 id="modal-title">Modal Title</h2>
            <p id="modal-description">Modal description text</p>
            
            <button type="button" 
                    class="modal-close" 
                    aria-label="Close modal">
                ×
            </button>
            
            <!-- Modal content -->
            
            <div class="modal-actions">
                <button type="button">Cancel</button>
                <button type="button">Confirm</button>
            </div>
        </div>
    </div>
    
    <script>
    // Trap focus within modal
    function trapFocus(element) {
        const focusableElements = element.querySelectorAll(
            'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])'
        );
        const firstFocusable = focusableElements[0];
        const lastFocusable = focusableElements[focusableElements.length - 1];
        
        element.addEventListener('keydown', function(e) {
            if (e.key === 'Tab') {
                if (e.shiftKey) { // Shift + Tab
                    if (document.activeElement === firstFocusable) {
                        lastFocusable.focus();
                        e.preventDefault();
                    }
                } else { // Tab
                    if (document.activeElement === lastFocusable) {
                        firstFocusable.focus();
                        e.preventDefault();
                    }
                }
            }
            
            if (e.key === 'Escape') {
                closeModal();
            }
        });
    }
    </script>
    <?php
}

Accessible Forms

Properly Labeled Form

<form role="form" aria-label="Contact form">
    <!-- Explicit label association -->
    <div class="form-group">
        <label for="name">
            Name <span aria-label="required">*</span>
        </label>
        <input type="text" 
               id="name" 
               name="name" 
               required 
               aria-required="true"
               aria-describedby="name-error" />
        <span id="name-error" role="alert" class="error"></span>
    </div>
    
    <!-- Grouped fields -->
    <fieldset>
        <legend>Preferred Contact Method</legend>
        <input type="radio" id="contact-email" name="contact" value="email">
        <label for="contact-email">Email</label>
        
        <input type="radio" id="contact-phone" name="contact" value="phone">
        <label for="contact-phone">Phone</label>
    </fieldset>
    
    <!-- Accessible error messages -->
    <div class="form-group">
        <label for="email">Email Address</label>
        <input type="email" 
               id="email" 
               aria-invalid="true"
               aria-describedby="email-error" />
        <span id="email-error" role="alert">
            Please enter a valid email address
        </span>
    </div>
    
    <button type="submit">Submit Form</button>
</form>

Color Contrast Requirements

Good Contrast

Ratio: 7.5:1 ✅

Poor Contrast

Ratio: 1.5:1 ❌

Element Type WCAG AA WCAG AAA
Normal Text 4.5:1 7:1
Large Text (18pt+) 3:1 4.5:1
UI Components 3:1 3:1

Keyboard Navigation

Keyboard-Accessible JavaScript

// Accessible dropdown menu
class AccessibleMenu {
    constructor(element) {
        this.menu = element;
        this.menuItems = element.querySelectorAll('[role="menuitem"]');
        this.currentIndex = -1;
        
        this.init();
    }
    
    init() {
        // Add keyboard event listeners
        this.menu.addEventListener('keydown', (e) => this.handleKeydown(e));
        
        // Add focus management
        this.menuItems.forEach((item, index) => {
            item.setAttribute('tabindex', index === 0 ? '0' : '-1');
            
            item.addEventListener('click', () => {
                this.setFocus(index);
            });
        });
    }
    
    handleKeydown(e) {
        switch(e.key) {
            case 'ArrowDown':
                e.preventDefault();
                this.setFocus(this.currentIndex + 1);
                break;
                
            case 'ArrowUp':
                e.preventDefault();
                this.setFocus(this.currentIndex - 1);
                break;
                
            case 'Home':
                e.preventDefault();
                this.setFocus(0);
                break;
                
            case 'End':
                e.preventDefault();
                this.setFocus(this.menuItems.length - 1);
                break;
                
            case 'Enter':
            case ' ':
                e.preventDefault();
                this.menuItems[this.currentIndex].click();
                break;
                
            case 'Escape':
                this.close();
                break;
        }
    }
    
    setFocus(index) {
        // Wrap around
        if (index < 0) index = this.menuItems.length - 1;
        if (index >= this.menuItems.length) index = 0;
        
        // Update tabindex
        this.menuItems.forEach((item, i) => {
            item.setAttribute('tabindex', i === index ? '0' : '-1');
        });
        
        // Set focus
        this.menuItems[index].focus();
        this.currentIndex = index;
    }
    
    close() {
        this.menu.setAttribute('aria-expanded', 'false');
        // Return focus to trigger element
    }
}

Accessibility Testing

🔍 WAVE

WebAIM's evaluation tool for accessibility issues

🎯 axe DevTools

Browser extension for automated testing

📊 Lighthouse

Chrome DevTools accessibility audit

🖥️ NVDA

Free screen reader for Windows

🍎 VoiceOver

Built-in macOS/iOS screen reader

🎨 Contrast Checker

Tools for testing color contrast ratios

WordPress Theme Accessibility Checklist

Structure & Semantics

Navigation

Content

Forms

Accessibility Best Practices

Development Best Practices

  • Design with accessibility in mind: Consider it from the start
  • Use semantic HTML: Let HTML do the heavy lifting
  • Test with real users: Include people with disabilities
  • Provide alternatives: Multiple ways to access content
  • Keep it simple: Complexity hurts accessibility
  • Test early and often: Use automated and manual testing
  • Document accessibility features: Help users find them
  • Stay updated: WCAG guidelines evolve
Accessibility overlays and plugins are not silver bullets. They cannot fix fundamental accessibility issues in your theme's code.

Practice Exercise

💻
Build Accessible Theme Components

Create fully accessible theme components:

  1. Implement skip links and test with keyboard
  2. Create accessible navigation with proper ARIA
  3. Build an accessible modal dialog
  4. Design accessible forms with validation
  5. Implement keyboard-navigable dropdown menu
  6. Add proper focus management
  7. Ensure WCAG AA color contrast
  8. Test with screen reader (NVDA/VoiceOver)
  9. Run accessibility audit with axe DevTools
  10. Achieve 100% accessibility score in Lighthouse

Additional Resources