Skip to main content

Course Progress

Loading...

♿ 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
💡
Key Statistic
Over 1 billion people worldwide have disabilities. Making your theme accessible opens your content to this significant audience.

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
Never remove focus indicators without providing a clear alternative. Keyboard users rely on focus indicators to navigate your site.
Test your theme with Windows Narrator, NVDA, JAWS, or macOS VoiceOver to understand how screen reader users experience your content.

Practice Exercise

💻
Make Your Theme Accessible

Implement accessibility features in your theme:

  1. Add skip links to main content
  2. Implement keyboard navigation for menu
  3. Add proper ARIA labels and roles
  4. Ensure color contrast meets WCAG AA
  5. Create accessible forms with proper labels
  6. Add screen reader text where needed
  7. Test with keyboard only (no mouse)
  8. Run axe DevTools audit
  9. Test with a screen reader
  10. Fix all accessibility issues found

Additional Resources