Back to blog
InspectionJanuary 1, 2024· 13 min read

Website Accessibility Testing and WCAG Compliance: A Developer's Guide

A practical Fusebox guide to website accessibility testing and wcag compliance.

Website Accessibility Testing and WCAG Compliance: A Developer's Guide

Web accessibility isn't just about compliance—it's about ensuring your website works for everyone. With 15% of the global population experiencing some form of disability, and accessibility lawsuits increasing by 23% annually, understanding WCAG (Web Content Accessibility Guidelines) compliance is crucial for modern web development.

Why Accessibility Testing Matters

In 2019, Domino's Pizza lost a Supreme Court case about their inaccessible website, setting a precedent that changed web development forever. Beyond legal risks, accessible websites have better SEO, improved usability for all users, and reach a wider audience. This guide provides practical tools for testing and improving your website's accessibility.

Quick Accessibility Audit

Run this in your browser console for an instant accessibility check:

// Quick accessibility audit
function quickAccessibilityAudit() {
    const issues = [];
    
    // Check images for alt text
    const imagesWithoutAlt = Array.from(document.querySelectorAll('img:not([alt])'));
    if (imagesWithoutAlt.length > 0) {
        issues.push({
            level: 'A',
            type: 'Images without alt text',
            count: imagesWithoutAlt.length,
            elements: imagesWithoutAlt.slice(0, 5)
        });
    }
    
    // Check for proper heading structure
    const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
    let lastLevel = 0;
    let headingIssues = 0;
    
    headings.forEach(heading => {
        const level = parseInt(heading.tagName[1]);
        if (level > lastLevel + 1) {
            headingIssues++;
        }
        lastLevel = level;
    });
    
    if (headingIssues > 0) {
        issues.push({
            level: 'A',
            type: 'Heading hierarchy issues',
            count: headingIssues,
            description: 'Headings skip levels'
        });
    }
    
    // Check form labels
    const inputsWithoutLabels = Array.from(document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"])')).filter(input => {
        const id = input.getAttribute('id');
        const hasLabel = id && document.querySelector(`label[for="${id}"]`);
        const hasAriaLabel = input.getAttribute('aria-label') || input.getAttribute('aria-labelledby');
        return !hasLabel && !hasAriaLabel;
    });
    
    if (inputsWithoutLabels.length > 0) {
        issues.push({
            level: 'A',
            type: 'Form inputs without labels',
            count: inputsWithoutLabels.length,
            elements: inputsWithoutLabels.slice(0, 5)
        });
    }
    
    // Check color contrast (basic check)
    const lowContrastElements = [];
    document.querySelectorAll('*').forEach(element => {
        const style = window.getComputedStyle(element);
        const bg = style.backgroundColor;
        const fg = style.color;
        
        if (bg !== 'rgba(0, 0, 0, 0)' && fg !== 'rgba(0, 0, 0, 0)') {
            // Simple contrast check (not WCAG accurate but indicative)
            const bgRGB = bg.match(/\d+/g);
            const fgRGB = fg.match(/\d+/g);
            
            if (bgRGB && fgRGB) {
                const bgLum = (0.299 * bgRGB[0] + 0.587 * bgRGB[1] + 0.114 * bgRGB[2]) / 255;
                const fgLum = (0.299 * fgRGB[0] + 0.587 * fgRGB[1] + 0.114 * fgRGB[2]) / 255;
                const contrast = Math.max(bgLum, fgLum) / Math.min(bgLum, fgLum);
                
                if (contrast < 3 && element.textContent.trim()) {
                    lowContrastElements.push(element);
                }
            }
        }
    });
    
    if (lowContrastElements.length > 0) {
        issues.push({
            level: 'AA',
            type: 'Potential contrast issues',
            count: lowContrastElements.length,
            elements: lowContrastElements.slice(0, 5)
        });
    }
    
    return {
        totalIssues: issues.reduce((sum, issue) => sum + issue.count, 0),
        issues,
        wcagLevels: {
            A: issues.filter(i => i.level === 'A').reduce((sum, i) => sum + i.count, 0),
            AA: issues.filter(i => i.level === 'AA').reduce((sum, i) => sum + i.count, 0)
        }
    };
}

quickAccessibilityAudit();

Keyboard Navigation Testing

Test keyboard accessibility comprehensively:

// Keyboard navigation tester
function testKeyboardNavigation() {
    const results = {
        totalInteractiveElements: 0,
        keyboardAccessible: 0,
        tabIndexIssues: [],
        focusIndicatorMissing: [],
        skipLinks: false
    };
    
    // Find all interactive elements
    const interactiveElements = document.querySelectorAll(
        'a, button, input, select, textarea, [tabindex], [role="button"], [role="link"]'
    );
    
    results.totalInteractiveElements = interactiveElements.length;
    
    // Check each element
    interactiveElements.forEach(element => {
        // Check if element is keyboard accessible
        const tabIndex = element.getAttribute('tabindex');
        const isNaturallyFocusable = ['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'].includes(element.tagName);
        
        if (tabIndex === '-1' && !element.closest('[aria-hidden="true"]')) {
            results.tabIndexIssues.push({
                element,
                issue: 'Element removed from tab order'
            });
        } else if (tabIndex > 0) {
            results.tabIndexIssues.push({
                element,
                issue: `Positive tabindex (${tabIndex}) disrupts natural order`
            });
        } else if (isNaturallyFocusable || tabIndex === '0') {
            results.keyboardAccessible++;
        }
        
        // Check for focus indicators
        element.addEventListener('focus', function checkFocus() {
            const styles = window.getComputedStyle(element);
            const focusStyles = window.getComputedStyle(element, ':focus');
            
            if (styles.outline === focusStyles.outline && 
                styles.boxShadow === focusStyles.boxShadow &&
                styles.border === focusStyles.border) {
                results.focusIndicatorMissing.push(element);
            }
            
            element.removeEventListener('focus', checkFocus);
        });
    });
    
    // Check for skip links
    const skipLink = document.querySelector('a[href^="#"][class*="skip"], a[href^="#main"], a[href^="#content"]');
    results.skipLinks = !!skipLink;
    
    // Test tab order
    const tabOrder = [];
    let currentElement = document.activeElement;
    
    // Simulate tabbing through page
    const tabTester = document.createElement('div');
    tabTester.innerHTML = '<button id="tab-test-start">Start</button>';
    document.body.appendChild(tabTester);
    document.getElementById('tab-test-start').focus();
    
    for (let i = 0; i < Math.min(50, interactiveElements.length); i++) {
        const event = new KeyboardEvent('keydown', { key: 'Tab', keyCode: 9 });
        document.activeElement.dispatchEvent(event);
        
        // Browser won't actually move focus, so we simulate
        const focusable = Array.from(interactiveElements).filter(el => {
            const tabIndex = el.getAttribute('tabindex');
            return tabIndex !== '-1' && !el.closest('[aria-hidden="true"]');
        });
        
        if (focusable[i]) {
            tabOrder.push({
                element: focusable[i],
                index: i,
                label: focusable[i].textContent.trim().substring(0, 30)
            });
        }
    }
    
    document.body.removeChild(tabTester);
    
    results.tabOrder = tabOrder;
    results.keyboardScore = Math.round((results.keyboardAccessible / results.totalInteractiveElements) * 100);
    
    return results;
}

testKeyboardNavigation();

Screen Reader Compatibility Check

Test how well your site works with screen readers:

// Screen reader compatibility analyzer
function analyzeScreenReaderCompatibility() {
    const analysis = {
        landmarkRegions: {},
        ariaUsage: {},
        semanticHTML: {},
        dynamicContent: {},
        formAccessibility: {},
        tableAccessibility: {}
    };
    
    // Check landmark regions
    const landmarks = {
        header: document.querySelectorAll('header, [role="banner"]').length,
        nav: document.querySelectorAll('nav, [role="navigation"]').length,
        main: document.querySelectorAll('main, [role="main"]').length,
        aside: document.querySelectorAll('aside, [role="complementary"]').length,
        footer: document.querySelectorAll('footer, [role="contentinfo"]').length
    };
    
    analysis.landmarkRegions = {
        found: landmarks,
        issues: []
    };
    
    if (landmarks.main !== 1) {
        analysis.landmarkRegions.issues.push('Should have exactly one main landmark');
    }
    if (landmarks.header === 0) {
        analysis.landmarkRegions.issues.push('Missing header/banner landmark');
    }
    
    // Check ARIA usage
    const ariaElements = document.querySelectorAll('[aria-label], [aria-labelledby], [aria-describedby], [role]');
    const ariaLive = document.querySelectorAll('[aria-live]');
    const ariaHidden = document.querySelectorAll('[aria-hidden="true"]');
    
    analysis.ariaUsage = {
        totalAriaAttributes: ariaElements.length,
        liveRegions: ariaLive.length,
        hiddenElements: ariaHidden.length,
        issues: []
    };
    
    // Check for ARIA issues
    ariaElements.forEach(element => {
        // Check for invalid ARIA attributes
        const role = element.getAttribute('role');
        if (role && !['button', 'link', 'navigation', 'main', 'banner', 'contentinfo', 'complementary', 'search', 'form', 'region'].includes(role)) {
            // More roles exist, but checking common ones
            const validRoles = element.tagName === 'DIV' || element.tagName === 'SPAN';
            if (!validRoles) {
                analysis.ariaUsage.issues.push({
                    element,
                    issue: `Potentially invalid role "${role}" on ${element.tagName}`
                });
            }
        }
        
        // Check aria-labelledby references
        const labelledBy = element.getAttribute('aria-labelledby');
        if (labelledBy && !document.getElementById(labelledBy)) {
            analysis.ariaUsage.issues.push({
                element,
                issue: `aria-labelledby references non-existent ID: ${labelledBy}`
            });
        }
    });
    
    // Check semantic HTML usage
    const semanticElements = {
        article: document.querySelectorAll('article').length,
        section: document.querySelectorAll('section').length,
        nav: document.querySelectorAll('nav').length,
        header: document.querySelectorAll('header').length,
        footer: document.querySelectorAll('footer').length,
        main: document.querySelectorAll('main').length,
        aside: document.querySelectorAll('aside').length,
        figure: document.querySelectorAll('figure').length
    };
    
    const nonSemanticButtons = document.querySelectorAll('div[onclick], span[onclick]').length;
    
    analysis.semanticHTML = {
        elements: semanticElements,
        score: Object.values(semanticElements).reduce((a, b) => a + b, 0),
        issues: []
    };
    
    if (nonSemanticButtons > 0) {
        analysis.semanticHTML.issues.push(`Found ${nonSemanticButtons} non-semantic clickable elements`);
    }
    
    // Check dynamic content
    const dynamicRegions = document.querySelectorAll('[aria-live], [aria-atomic], [role="alert"], [role="status"]');
    
    analysis.dynamicContent = {
        liveRegions: dynamicRegions.length,
        alerts: document.querySelectorAll('[role="alert"]').length,
        status: document.querySelectorAll('[role="status"]').length,
        recommendations: []
    };
    
    if (dynamicRegions.length === 0) {
        analysis.dynamicContent.recommendations.push('No live regions found for dynamic content updates');
    }
    
    // Check form accessibility
    const forms = document.querySelectorAll('form');
    let accessibleForms = 0;
    
    forms.forEach(form => {
        const hasFieldset = form.querySelector('fieldset');
        const hasLabels = form.querySelectorAll('input:not([type="hidden"])').length === 
                         form.querySelectorAll('label').length;
        
        if (hasFieldset || hasLabels) {
            accessibleForms++;
        }
    });
    
    analysis.formAccessibility = {
        totalForms: forms.length,
        accessibleForms,
        score: forms.length ? Math.round((accessibleForms / forms.length) * 100) : 100
    };
    
    // Check table accessibility
    const tables = document.querySelectorAll('table');
    let accessibleTables = 0;
    
    tables.forEach(table => {
        const hasCaption = !!table.querySelector('caption');
        const hasHeaders = table.querySelectorAll('th').length > 0;
        const hasScope = Array.from(table.querySelectorAll('th')).some(th => th.hasAttribute('scope'));
        
        if (hasCaption || (hasHeaders && hasScope)) {
            accessibleTables++;
        }
    });
    
    analysis.tableAccessibility = {
        totalTables: tables.length,
        accessibleTables,
        score: tables.length ? Math.round((accessibleTables / tables.length) * 100) : 100
    };
    
    return analysis;
}

analyzeScreenReaderCompatibility();

Color Contrast Analyzer

Accurately check WCAG color contrast requirements:

// WCAG Color Contrast Analyzer
function analyzeColorContrast() {
    // Calculate relative luminance
    function getLuminance(r, g, b) {
        const [rs, gs, bs] = [r, g, b].map(c => {
            c = c / 255;
            return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
        });
        return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
    }
    
    // Calculate contrast ratio
    function getContrastRatio(rgb1, rgb2) {
        const lum1 = getLuminance(...rgb1);
        const lum2 = getLuminance(...rgb2);
        const brightest = Math.max(lum1, lum2);
        const darkest = Math.min(lum1, lum2);
        return (brightest + 0.05) / (darkest + 0.05);
    }
    
    // Parse color to RGB
    function parseColor(color) {
        const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
        return match ? [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])] : null;
    }
    
    const results = {
        totalTextElements: 0,
        passed: { AA: 0, AAA: 0 },
        failed: { AA: [], AAA: [] },
        warnings: []
    };
    
    // Get all text elements
    const textElements = Array.from(document.querySelectorAll('*')).filter(el => {
        const text = el.textContent.trim();
        const hasText = text && el.childNodes.length === 1 && el.childNodes[0].nodeType === 3;
        return hasText && window.getComputedStyle(el).display !== 'none';
    });
    
    results.totalTextElements = textElements.length;
    
    textElements.forEach(element => {
        const styles = window.getComputedStyle(element);
        const fgColor = parseColor(styles.color);
        const bgColor = parseColor(styles.backgroundColor);
        
        if (fgColor && bgColor) {
            const ratio = getContrastRatio(fgColor, bgColor);
            const fontSize = parseFloat(styles.fontSize);
            const fontWeight = styles.fontWeight;
            const isLargeText = fontSize >= 18 || (fontSize >= 14 && fontWeight >= 700);
            
            // WCAG requirements
            const aaRequired = isLargeText ? 3 : 4.5;
            const aaaRequired = isLargeText ? 4.5 : 7;
            
            const passedAA = ratio >= aaRequired;
            const passedAAA = ratio >= aaaRequired;
            
            if (passedAA) results.passed.AA++;
            if (passedAAA) results.passed.AAA++;
            
            if (!passedAA) {
                results.failed.AA.push({
                    element,
                    text: element.textContent.substring(0, 50),
                    ratio: ratio.toFixed(2),
                    required: aaRequired,
                    colors: { foreground: styles.color, background: styles.backgroundColor }
                });
            }
            
            if (!passedAAA) {
                results.failed.AAA.push({
                    element,
                    ratio: ratio.toFixed(2),
                    required: aaaRequired
                });
            }
        }
    });
    
    // Check for color-only information
    const links = document.querySelectorAll('a');
    links.forEach(link => {
        const styles = window.getComputedStyle(link);
        const hasUnderline = styles.textDecoration.includes('underline');
        const hasBorder = styles.borderBottomStyle !== 'none';
        
        if (!hasUnderline && !hasBorder) {
            results.warnings.push({
                element: link,
                issue: 'Link may rely on color alone for identification'
            });
        }
    });
    
    results.summary = {
        aaCompliance: Math.round((results.passed.AA / results.totalTextElements) * 100),
        aaaCompliance: Math.round((results.passed.AAA / results.totalTextElements) * 100),
        criticalIssues: results.failed.AA.length
    };
    
    return results;
}

analyzeColorContrast();

Focus Management Analyzer

Test focus management for dynamic content:

// Focus management analyzer
function analyzeFocusManagement() {
    const analysis = {
        focusTraps: [],
        modalAccessibility: {},
        focusRestoration: [],
        tabPanels: {}
    };
    
    // Detect potential focus traps
    const detectFocusTraps = () => {
        const modals = document.querySelectorAll('[role="dialog"], .modal, [class*="modal"]');
        
        modals.forEach(modal => {
            const focusableElements = modal.querySelectorAll(
                'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
            );
            
            if (focusableElements.length > 0) {
                const firstFocusable = focusableElements[0];
                const lastFocusable = focusableElements[focusableElements.length - 1];
                
                // Check if focus is trapped
                const hasTrap = modal.hasAttribute('aria-modal') || 
                               modal.style.position === 'fixed';
                
                analysis.focusTraps.push({
                    element: modal,
                    focusableCount: focusableElements.length,
                    hasProperTrap: hasTrap,
                    firstFocusable,
                    lastFocusable
                });
            }
        });
    };
    
    // Check modal accessibility
    const checkModalAccessibility = () => {
        const modals = document.querySelectorAll('[role="dialog"], .modal');
        
        modals.forEach(modal => {
            const hasLabel = modal.hasAttribute('aria-label') || 
                           modal.hasAttribute('aria-labelledby');
            const hasDescription = modal.hasAttribute('aria-describedby');
            const isModal = modal.getAttribute('aria-modal') === 'true';
            
            analysis.modalAccessibility[modal.id || 'unnamed'] = {
                hasLabel,
                hasDescription,
                isProperlyMarked: isModal,
                issues: []
            };
            
            if (!hasLabel) {
                analysis.modalAccessibility[modal.id || 'unnamed'].issues.push('Missing accessible label');
            }
            if (!isModal) {
                analysis.modalAccessibility[modal.id || 'unnamed'].issues.push('Missing aria-modal="true"');
            }
        });
    };
    
    // Check tab panels
    const checkTabPanels = () => {
        const tabLists = document.querySelectorAll('[role="tablist"]');
        
        tabLists.forEach(tabList => {
            const tabs = tabList.querySelectorAll('[role="tab"]');
            const panels = [];
            
            tabs.forEach(tab => {
                const controls = tab.getAttribute('aria-controls');
                if (controls) {
                    const panel = document.getElementById(controls);
                    if (panel) {
                        panels.push({
                            tab,
                            panel,
                            hasProperRole: panel.getAttribute('role') === 'tabpanel',
                            isLabelledBy: panel.getAttribute('aria-labelledby') === tab.id
                        });
                    }
                }
            });
            
            analysis.tabPanels[tabList.id || 'unnamed'] = {
                tabCount: tabs.length,
                panelCount: panels.length,
                properImplementation: panels.every(p => p.hasProperRole && p.isLabelledBy)
            };
        });
    };
    
    detectFocusTraps();
    checkModalAccessibility();
    checkTabPanels();
    
    return analysis;
}

analyzeFocusManagement();

Complete WCAG 2.1 Compliance Audit

Comprehensive accessibility audit function:

// Complete WCAG 2.1 Compliance Audit
async function performWCAGAudit() {
    console.log('Starting WCAG 2.1 Compliance Audit...');
    
    const audit = {
        timestamp: new Date().toISOString(),
        url: window.location.href,
        wcagVersion: '2.1',
        levels: { A: [], AA: [], AAA: [] },
        summary: {},
        score: 0
    };
    
    // 1. Perceivable
    console.log('Testing Perceivable criteria...');
    
    // 1.1.1 Non-text Content (Level A)
    const imagesWithoutAlt = document.querySelectorAll('img:not([alt])');
    if (imagesWithoutAlt.length > 0) {
        audit.levels.A.push({
            criterion: '1.1.1',
            name: 'Non-text Content',
            issues: `${imagesWithoutAlt.length} images without alt text`,
            elements: Array.from(imagesWithoutAlt).slice(0, 3)
        });
    }
    
    // 1.4.3 Contrast Minimum (Level AA)
    const contrastResults = analyzeColorContrast();
    if (contrastResults.failed.AA.length > 0) {
        audit.levels.AA.push({
            criterion: '1.4.3',
            name: 'Contrast (Minimum)',
            issues: `${contrastResults.failed.AA.length} elements fail contrast requirements`,
            details: contrastResults.failed.AA.slice(0, 3)
        });
    }
    
    // 2. Operable
    console.log('Testing Operable criteria...');
    
    // 2.1.1 Keyboard (Level A)
    const keyboardResults = testKeyboardNavigation();
    if (keyboardResults.tabIndexIssues.length > 0) {
        audit.levels.A.push({
            criterion: '2.1.1',
            name: 'Keyboard',
            issues: `${keyboardResults.tabIndexIssues.length} keyboard accessibility issues`,
            details: keyboardResults.tabIndexIssues.slice(0, 3)
        });
    }
    
    // 2.4.1 Bypass Blocks (Level A)
    if (!keyboardResults.skipLinks) {
        audit.levels.A.push({
            criterion: '2.4.1',
            name: 'Bypass Blocks',
            issues: 'No skip links found'
        });
    }
    
    // 2.4.6 Headings and Labels (Level AA)
    const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
    const emptyHeadings = Array.from(headings).filter(h => !h.textContent.trim());
    if (emptyHeadings.length > 0) {
        audit.levels.AA.push({
            criterion: '2.4.6',
            name: 'Headings and Labels',
            issues: `${emptyHeadings.length} empty headings found`
        });
    }
    
    // 3. Understandable
    console.log('Testing Understandable criteria...');
    
    // 3.1.1 Language of Page (Level A)
    const htmlLang = document.documentElement.getAttribute('lang');
    if (!htmlLang) {
        audit.levels.A.push({
            criterion: '3.1.1',
            name: 'Language of Page',
            issues: 'Missing lang attribute on html element'
        });
    }
    
    // 3.3.2 Labels or Instructions (Level A)
    const inputsWithoutLabels = document.querySelectorAll('input:not([type="hidden"]):not([type="submit"])');
    let unlabeledInputs = 0;
    
    inputsWithoutLabels.forEach(input => {
        const hasLabel = input.labels && input.labels.length > 0;
        const hasAriaLabel = input.hasAttribute('aria-label') || input.hasAttribute('aria-labelledby');
        
        if (!hasLabel && !hasAriaLabel) {
            unlabeledInputs++;
        }
    });
    
    if (unlabeledInputs > 0) {
        audit.levels.A.push({
            criterion: '3.3.2',
            name: 'Labels or Instructions',
            issues: `${unlabeledInputs} form inputs without labels`
        });
    }
    
    // 4. Robust
    console.log('Testing Robust criteria...');
    
    // 4.1.2 Name, Role, Value (Level A)
    const customControls = document.querySelectorAll('[onclick]:not(button):not(a):not(input)');
    const improperControls = Array.from(customControls).filter(el => {
        return !el.hasAttribute('role') && !el.hasAttribute('aria-label');
    });
    
    if (improperControls.length > 0) {
        audit.levels.A.push({
            criterion: '4.1.2',
            name: 'Name, Role, Value',
            issues: `${improperControls.length} custom controls without proper ARIA`,
            elements: improperControls.slice(0, 3)
        });
    }
    
    // Calculate score
    const weights = { A: 40, AA: 30, AAA: 10 };
    const maxScore = 100;
    let deductions = 0;
    
    Object.entries(audit.levels).forEach(([level, issues]) => {
        deductions += issues.length * (weights[level] / 10);
    });
    
    audit.score = Math.max(0, Math.round(maxScore - deductions));
    
    // Generate summary
    audit.summary = {
        score: audit.score,
        levelA: audit.levels.A.length,
        levelAA: audit.levels.AA.length,
        levelAAA: audit.levels.AAA.length,
        totalIssues: audit.levels.A.length + audit.levels.AA.length + audit.levels.AAA.length,
        criticalIssues: audit.levels.A.length,
        recommendation: audit.score >= 90 ? 'Excellent' : 
                       audit.score >= 70 ? 'Good' : 
                       audit.score >= 50 ? 'Needs Improvement' : 'Poor'
    };
    
    // Add specific recommendations
    audit.recommendations = [];
    
    if (audit.levels.A.length > 0) {
        audit.recommendations.push('Fix Level A issues immediately - these are critical for basic accessibility');
    }
    
    if (contrastResults && contrastResults.failed.AA.length > 0) {
        audit.recommendations.push('Improve color contrast ratios to meet WCAG AA standards');
    }
    
    if (!keyboardResults || keyboardResults.keyboardScore < 100) {
        audit.recommendations.push('Ensure all interactive elements are keyboard accessible');
    }
    
    console.log('WCAG Audit Complete!');
    return audit;
}

// Run the audit
performWCAGAudit().then(console.log);

Mobile Accessibility Checker

Test mobile-specific accessibility:

// Mobile accessibility checker
function checkMobileAccessibility() {
    const results = {
        touchTargets: {},
        viewport: {},
        orientation: {},
        gestures: {}
    };
    
    // Check touch target sizes (WCAG 2.1 - 2.5.5)
    const interactiveElements = document.querySelectorAll('a, button, input, select, [role="button"]');
    let smallTargets = 0;
    
    interactiveElements.forEach(element => {
        const rect = element.getBoundingClientRect();
        const width = rect.width;
        const height = rect.height;
        
        // WCAG 2.1 recommends 44x44 CSS pixels minimum
        if (width < 44 || height < 44) {
            smallTargets++;
        }
    });
    
    results.touchTargets = {
        total: interactiveElements.length,
        tooSmall: smallTargets,
        percentage: Math.round((smallTargets / interactiveElements.length) * 100)
    };
    
    // Check viewport settings
    const viewport = document.querySelector('meta[name="viewport"]');
    results.viewport = {
        exists: !!viewport,
        content: viewport ? viewport.getAttribute('content') : null,
        allowsZoom: !viewport || !viewport.getAttribute('content').includes('user-scalable=no')
    };
    
    // Check orientation handling
    results.orientation = {
        locked: !!document.querySelector('meta[name="screen-orientation"]'),
        responsive: window.matchMedia('(orientation: portrait)').matches !== 
                   window.matchMedia('(orientation: landscape)').matches
    };
    
    return results;
}

checkMobileAccessibility();

Best Practices for Accessibility

  1. Start with Semantic HTML: Use proper HTML elements for their intended purpose
  2. Provide Text Alternatives: All images need descriptive alt text
  3. Ensure Keyboard Access: Everything mouse users can do, keyboard users should too
  4. Use Sufficient Color Contrast: 4.5:1 for normal text, 3:1 for large text
  5. Make Forms Accessible: Every input needs a label
  6. Structure Content Properly: Use headings hierarchically
  7. Provide Focus Indicators: Never remove focus outlines without replacement
  8. Test with Screen Readers: Use NVDA, JAWS, or VoiceOver
  9. Support Browser Zoom: Content should be usable at 200% zoom
  10. Write Clear Language: Keep content at an appropriate reading level

Common Accessibility Issues

  • Missing Alt Text: Screen readers can't describe images
  • Poor Color Contrast: Text is hard to read
  • Keyboard Traps: Users can't navigate away from elements
  • Missing Form Labels: Users don't know what to enter
  • Auto-playing Media: Disrupts screen reader users
  • Inaccessible PDFs: Often completely unusable by screen readers
  • Missing Language Declaration: Screen readers mispronounce content
  • Touch Targets Too Small: Mobile users can't accurately tap
  • Missing Skip Links: Keyboard users must tab through entire navigation
  • Improper ARIA Usage: Can make things worse than no ARIA

Remember: Accessibility isn't a feature—it's a fundamental aspect of web development. These tools help identify issues, but true accessibility comes from understanding and empathizing with all users' needs. Test with real assistive technologies and, most importantly, with real users who rely on them.