Web Accessibility Best Practices - Building Inclusive Digital Experiences

Web Accessibility Best Practices - Building Inclusive Digital Experiences

Web accessibility ensures that digital experiences are usable by everyone, including people with disabilities. Beyond legal compliance, accessible design creates better user experiences for all users and demonstrates social responsibility. This comprehensive guide explores modern accessibility practices, WCAG compliance strategies, and techniques for building truly inclusive web applications.

Understanding Web Accessibility Fundamentals

Web accessibility is built on four core principles known as POUR: Perceivable, Operable, Understandable, and Robust. These principles guide all accessibility implementations.

<!-- Semantic HTML foundation for accessibility -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>JobFinders - Accessible Job Search Platform</title>
  
  <!-- Skip navigation for screen readers -->
  <a href="#main-content" class="skip-link">Skip to main content</a>
</head>
<body>
  <!-- Landmark navigation -->
  <nav role="navigation" aria-label="Main navigation">
    <ul>
      <li><a href="/" aria-current="page">Home</a></li>
      <li><a href="/search">Search Jobs</a></li>
      <li><a href="/applications">My Applications</a></li>
      <li><a href="/profile">Profile</a></li>
    </ul>
  </nav>
  
  <!-- Main content area -->
  <main id="main-content" role="main">
    <h1>Find Your Next Career Opportunity</h1>
    
    <!-- Search form with proper labeling -->
    <form role="search" aria-label="Job search">
      <fieldset>
        <legend>Search Criteria</legend>
        
        <div class="form-group">
          <label for="job-title">Job Title</label>
          <input 
            type="text" 
            id="job-title" 
            name="jobTitle"
            aria-describedby="job-title-help"
            autocomplete="organization-title"
          />
          <div id="job-title-help" class="help-text">
            Enter keywords for the position you're seeking
          </div>
        </div>
        
        <div class="form-group">
          <label for="location">Location</label>
          <input 
            type="text" 
            id="location" 
            name="location"
            aria-describedby="location-help"
            autocomplete="address-level2"
          />
          <div id="location-help" class="help-text">
            City, state, or "remote" for remote positions
          </div>
        </div>
        
        <button type="submit" aria-describedby="search-results-count">
          Search Jobs
        </button>
        
        <div id="search-results-count" aria-live="polite" aria-atomic="true">
          <!-- Results count will be announced to screen readers -->
        </div>
      </fieldset>
    </form>
    
    <!-- Results section with proper headings -->
    <section aria-labelledby="results-heading">
      <h2 id="results-heading">Search Results</h2>
      
      <div class="results-container" role="region" aria-label="Job listings">
        <!-- Job cards with accessible structure -->
        <article class="job-card" tabindex="0">
          <header>
            <h3>
              <a href="/jobs/123" aria-describedby="job-123-details">
                Senior Software Developer
              </a>
            </h3>
            <p class="company">Custom Logic Solutions</p>
          </header>
          
          <div id="job-123-details" class="job-details">
            <p class="location">Remote</p>
            <p class="salary" aria-label="Salary range">$80,000 - $120,000</p>
            <p class="posted-date">
              <time datetime="2025-01-15">Posted 2 days ago</time>
            </p>
          </div>
          
          <div class="job-actions">
            <button 
              type="button" 
              aria-label="Save Senior Software Developer position at Custom Logic Solutions"
              data-job-id="123"
            >
              Save Job
            </button>
            
            <button 
              type="button" 
              aria-label="Apply to Senior Software Developer position at Custom Logic Solutions"
              data-job-id="123"
            >
              Apply Now
            </button>
          </div>
        </article>
      </div>
    </section>
  </main>
  
  <!-- Accessible footer -->
  <footer role="contentinfo">
    <nav aria-label="Footer navigation">
      <ul>
        <li><a href="/about">About Us</a></li>
        <li><a href="/privacy">Privacy Policy</a></li>
        <li><a href="/accessibility">Accessibility Statement</a></li>
        <li><a href="/contact">Contact Support</a></li>
      </ul>
    </nav>
  </footer>
</body>
</html>

ARIA (Accessible Rich Internet Applications) Implementation

ARIA attributes enhance semantic meaning and provide additional context for assistive technologies.

// Advanced ARIA implementation for dynamic content
class AccessibleUIComponents {
  constructor() {
    this.init();
  }
  
  init() {
    this.setupAccessibleModals();
    this.setupAccessibleDropdowns();
    this.setupAccessibleTabs();
    this.setupLiveRegions();
    this.setupAccessibleForms();
  }
  
  // Accessible modal dialogs
  setupAccessibleModals() {
    const modalTriggers = document.querySelectorAll('[data-modal-trigger]');
    
    modalTriggers.forEach(trigger => {
      trigger.addEventListener('click', (e) => {
        e.preventDefault();
        const modalId = trigger.getAttribute('data-modal-trigger');
        this.openModal(modalId);
      });
    });
  }
  
  openModal(modalId) {
    const modal = document.getElementById(modalId);
    if (!modal) return;
    
    // Store the element that triggered the modal
    const activeElement = document.activeElement;
    modal.setAttribute('data-previous-focus', activeElement.id || '');
    
    // Set ARIA attributes
    modal.setAttribute('aria-hidden', 'false');
    modal.setAttribute('role', 'dialog');
    modal.setAttribute('aria-modal', 'true');
    
    // Find and set aria-labelledby
    const modalTitle = modal.querySelector('h1, h2, h3, .modal-title');
    if (modalTitle) {
      if (!modalTitle.id) {
        modalTitle.id = `modal-title-${Date.now()}`;
      }
      modal.setAttribute('aria-labelledby', modalTitle.id);
    }
    
    // Show modal
    modal.classList.add('modal-open');
    
    // Focus management
    this.trapFocus(modal);
    
    // Focus first focusable element
    const firstFocusable = modal.querySelector(`
      button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])
    `);
    if (firstFocusable) {
      firstFocusable.focus();
    }
    
    // Close on Escape key
    const handleEscape = (e) => {
      if (e.key === 'Escape') {
        this.closeModal(modalId);
        document.removeEventListener('keydown', handleEscape);
      }
    };
    document.addEventListener('keydown', handleEscape);
    
    // Close on backdrop click
    modal.addEventListener('click', (e) => {
      if (e.target === modal) {
        this.closeModal(modalId);
      }
    });
  }
  
  closeModal(modalId) {
    const modal = document.getElementById(modalId);
    if (!modal) return;
    
    // Hide modal
    modal.setAttribute('aria-hidden', 'true');
    modal.classList.remove('modal-open');
    
    // Restore focus
    const previousFocusId = modal.getAttribute('data-previous-focus');
    if (previousFocusId) {
      const previousElement = document.getElementById(previousFocusId);
      if (previousElement) {
        previousElement.focus();
      }
    }
    
    // Remove focus trap
    this.removeFocusTrap(modal);
  }
  
  trapFocus(container) {
    const focusableElements = container.querySelectorAll(`
      button:not([disabled]), 
      [href], 
      input:not([disabled]), 
      select:not([disabled]), 
      textarea:not([disabled]), 
      [tabindex]:not([tabindex="-1"]):not([disabled])
    `);
    
    const firstElement = focusableElements[0];
    const lastElement = focusableElements[focusableElements.length - 1];
    
    const handleTabKey = (e) => {
      if (e.key === 'Tab') {
        if (e.shiftKey) {
          // Shift + Tab
          if (document.activeElement === firstElement) {
            e.preventDefault();
            lastElement.focus();
          }
        } else {
          // Tab
          if (document.activeElement === lastElement) {
            e.preventDefault();
            firstElement.focus();
          }
        }
      }
    };
    
    container.addEventListener('keydown', handleTabKey);
    container.setAttribute('data-focus-trap', 'true');
  }
  
  removeFocusTrap(container) {
    container.removeAttribute('data-focus-trap');
    // Remove event listeners would be handled by cloning and replacing
    // or by storing references to remove specific listeners
  }
  
  // Accessible dropdown menus
  setupAccessibleDropdowns() {
    const dropdowns = document.querySelectorAll('.dropdown');
    
    dropdowns.forEach(dropdown => {
      const trigger = dropdown.querySelector('.dropdown-trigger');
      const menu = dropdown.querySelector('.dropdown-menu');
      
      if (!trigger || !menu) return;
      
      // Set ARIA attributes
      trigger.setAttribute('aria-haspopup', 'true');
      trigger.setAttribute('aria-expanded', 'false');
      
      if (!menu.id) {
        menu.id = `dropdown-menu-${Date.now()}`;
      }
      trigger.setAttribute('aria-controls', menu.id);
      
      menu.setAttribute('role', 'menu');
      menu.querySelectorAll('a, button').forEach(item => {
        item.setAttribute('role', 'menuitem');
      });
      
      // Event handlers
      trigger.addEventListener('click', () => {
        this.toggleDropdown(dropdown);
      });
      
      trigger.addEventListener('keydown', (e) => {
        if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          this.openDropdown(dropdown);
        }
      });
      
      // Menu navigation
      menu.addEventListener('keydown', (e) => {
        this.handleMenuNavigation(e, menu);
      });
    });
  }
  
  toggleDropdown(dropdown) {
    const trigger = dropdown.querySelector('.dropdown-trigger');
    const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
    
    if (isExpanded) {
      this.closeDropdown(dropdown);
    } else {
      this.openDropdown(dropdown);
    }
  }
  
  openDropdown(dropdown) {
    const trigger = dropdown.querySelector('.dropdown-trigger');
    const menu = dropdown.querySelector('.dropdown-menu');
    
    trigger.setAttribute('aria-expanded', 'true');
    menu.classList.add('dropdown-open');
    
    // Focus first menu item
    const firstItem = menu.querySelector('[role="menuitem"]');
    if (firstItem) {
      firstItem.focus();
    }
  }
  
  closeDropdown(dropdown) {
    const trigger = dropdown.querySelector('.dropdown-trigger');
    const menu = dropdown.querySelector('.dropdown-menu');
    
    trigger.setAttribute('aria-expanded', 'false');
    menu.classList.remove('dropdown-open');
    trigger.focus();
  }
  
  handleMenuNavigation(e, menu) {
    const items = menu.querySelectorAll('[role="menuitem"]');
    const currentIndex = Array.from(items).indexOf(document.activeElement);
    
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        const nextIndex = (currentIndex + 1) % items.length;
        items[nextIndex].focus();
        break;
        
      case 'ArrowUp':
        e.preventDefault();
        const prevIndex = (currentIndex - 1 + items.length) % items.length;
        items[prevIndex].focus();
        break;
        
      case 'Escape':
        e.preventDefault();
        this.closeDropdown(menu.closest('.dropdown'));
        break;
        
      case 'Home':
        e.preventDefault();
        items[0].focus();
        break;
        
      case 'End':
        e.preventDefault();
        items[items.length - 1].focus();
        break;
    }
  }
  
  // Accessible tab panels
  setupAccessibleTabs() {
    const tabContainers = document.querySelectorAll('.tab-container');
    
    tabContainers.forEach(container => {
      const tabList = container.querySelector('.tab-list');
      const tabs = container.querySelectorAll('.tab');
      const panels = container.querySelectorAll('.tab-panel');
      
      // Set ARIA attributes
      tabList.setAttribute('role', 'tablist');
      
      tabs.forEach((tab, index) => {
        tab.setAttribute('role', 'tab');
        tab.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
        tab.setAttribute('tabindex', index === 0 ? '0' : '-1');
        
        if (!tab.id) {
          tab.id = `tab-${Date.now()}-${index}`;
        }
        
        const panel = panels[index];
        if (panel) {
          if (!panel.id) {
            panel.id = `panel-${Date.now()}-${index}`;
          }
          
          tab.setAttribute('aria-controls', panel.id);
          panel.setAttribute('role', 'tabpanel');
          panel.setAttribute('aria-labelledby', tab.id);
          panel.setAttribute('tabindex', '0');
          
          // Hide inactive panels
          if (index !== 0) {
            panel.hidden = true;
          }
        }
      });
      
      // Event handlers
      tabList.addEventListener('click', (e) => {
        const tab = e.target.closest('.tab');
        if (tab) {
          this.activateTab(tab, tabs, panels);
        }
      });
      
      tabList.addEventListener('keydown', (e) => {
        this.handleTabNavigation(e, tabs);
      });
    });
  }
  
  activateTab(activeTab, allTabs, allPanels) {
    const activeIndex = Array.from(allTabs).indexOf(activeTab);
    
    // Update tabs
    allTabs.forEach((tab, index) => {
      const isActive = index === activeIndex;
      tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
      tab.setAttribute('tabindex', isActive ? '0' : '-1');
    });
    
    // Update panels
    allPanels.forEach((panel, index) => {
      panel.hidden = index !== activeIndex;
    });
    
    // Focus active tab
    activeTab.focus();
  }
  
  handleTabNavigation(e, tabs) {
    const currentIndex = Array.from(tabs).findIndex(tab => 
      tab.getAttribute('aria-selected') === 'true'
    );
    
    let newIndex = currentIndex;
    
    switch (e.key) {
      case 'ArrowLeft':
        e.preventDefault();
        newIndex = (currentIndex - 1 + tabs.length) % tabs.length;
        break;
        
      case 'ArrowRight':
        e.preventDefault();
        newIndex = (currentIndex + 1) % tabs.length;
        break;
        
      case 'Home':
        e.preventDefault();
        newIndex = 0;
        break;
        
      case 'End':
        e.preventDefault();
        newIndex = tabs.length - 1;
        break;
    }
    
    if (newIndex !== currentIndex) {
      this.activateTab(tabs[newIndex], tabs, 
        document.querySelectorAll('.tab-panel'));
    }
  }
  
  // Live regions for dynamic content updates
  setupLiveRegions() {
    // Create status region for announcements
    const statusRegion = document.createElement('div');
    statusRegion.id = 'status-region';
    statusRegion.setAttribute('aria-live', 'polite');
    statusRegion.setAttribute('aria-atomic', 'true');
    statusRegion.className = 'sr-only';
    document.body.appendChild(statusRegion);
    
    // Create alert region for urgent messages
    const alertRegion = document.createElement('div');
    alertRegion.id = 'alert-region';
    alertRegion.setAttribute('aria-live', 'assertive');
    alertRegion.setAttribute('aria-atomic', 'true');
    alertRegion.className = 'sr-only';
    document.body.appendChild(alertRegion);
  }
  
  announceToScreenReader(message, priority = 'polite') {
    const regionId = priority === 'assertive' ? 'alert-region' : 'status-region';
    const region = document.getElementById(regionId);
    
    if (region) {
      // Clear previous message
      region.textContent = '';
      
      // Add new message after a brief delay to ensure it's announced
      setTimeout(() => {
        region.textContent = message;
      }, 100);
      
      // Clear message after announcement
      setTimeout(() => {
        region.textContent = '';
      }, 5000);
    }
  }
  
  // Accessible form validation
  setupAccessibleForms() {
    const forms = document.querySelectorAll('form[data-accessible-validation]');
    
    forms.forEach(form => {
      const inputs = form.querySelectorAll('input, select, textarea');
      
      inputs.forEach(input => {
        // Add error container
        if (!input.nextElementSibling?.classList.contains('error-message')) {
          const errorContainer = document.createElement('div');
          errorContainer.className = 'error-message';
          errorContainer.id = `${input.id}-error`;
          errorContainer.setAttribute('role', 'alert');
          errorContainer.setAttribute('aria-live', 'polite');
          input.parentNode.insertBefore(errorContainer, input.nextSibling);
        }
        
        // Validation on blur
        input.addEventListener('blur', () => {
          this.validateField(input);
        });
        
        // Clear errors on input
        input.addEventListener('input', () => {
          this.clearFieldError(input);
        });
      });
      
      // Form submission
      form.addEventListener('submit', (e) => {
        if (!this.validateForm(form)) {
          e.preventDefault();
          this.focusFirstError(form);
        }
      });
    });
  }
  
  validateField(input) {
    const errorContainer = document.getElementById(`${input.id}-error`);
    let errorMessage = '';
    
    // Required field validation
    if (input.hasAttribute('required') && !input.value.trim()) {
      errorMessage = `${this.getFieldLabel(input)} is required`;
    }
    
    // Email validation
    if (input.type === 'email' && input.value && !this.isValidEmail(input.value)) {
      errorMessage = 'Please enter a valid email address';
    }
    
    // Custom validation
    if (input.hasAttribute('data-validation-pattern')) {
      const pattern = new RegExp(input.getAttribute('data-validation-pattern'));
      if (input.value && !pattern.test(input.value)) {
        errorMessage = input.getAttribute('data-validation-message') || 'Invalid format';
      }
    }
    
    if (errorMessage) {
      this.showFieldError(input, errorMessage);
      return false;
    } else {
      this.clearFieldError(input);
      return true;
    }
  }
  
  showFieldError(input, message) {
    const errorContainer = document.getElementById(`${input.id}-error`);
    
    input.setAttribute('aria-invalid', 'true');
    input.setAttribute('aria-describedby', `${input.id}-error`);
    input.classList.add('error');
    
    errorContainer.textContent = message;
    errorContainer.style.display = 'block';
  }
  
  clearFieldError(input) {
    const errorContainer = document.getElementById(`${input.id}-error`);
    
    input.removeAttribute('aria-invalid');
    input.removeAttribute('aria-describedby');
    input.classList.remove('error');
    
    errorContainer.textContent = '';
    errorContainer.style.display = 'none';
  }
  
  validateForm(form) {
    const inputs = form.querySelectorAll('input, select, textarea');
    let isValid = true;
    
    inputs.forEach(input => {
      if (!this.validateField(input)) {
        isValid = false;
      }
    });
    
    return isValid;
  }
  
  focusFirstError(form) {
    const firstError = form.querySelector('[aria-invalid="true"]');
    if (firstError) {
      firstError.focus();
      
      // Announce error count
      const errorCount = form.querySelectorAll('[aria-invalid="true"]').length;
      this.announceToScreenReader(
        `Form has ${errorCount} error${errorCount > 1 ? 's' : ''}. Please correct and try again.`,
        'assertive'
      );
    }
  }
  
  getFieldLabel(input) {
    const label = document.querySelector(`label[for="${input.id}"]`);
    return label ? label.textContent.trim() : input.name || 'Field';
  }
  
  isValidEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
}

// Initialize accessible components
const accessibleUI = new AccessibleUIComponents();

// Example usage: Announce dynamic content changes
document.addEventListener('DOMContentLoaded', () => {
  // Announce search results
  const searchForm = document.querySelector('form[role="search"]');
  if (searchForm) {
    searchForm.addEventListener('submit', async (e) => {
      e.preventDefault();
      
      // Perform search
      const results = await performJobSearch();
      
      // Update results count
      const resultsCount = document.getElementById('search-results-count');
      const count = results.length;
      const message = count === 0 
        ? 'No jobs found matching your criteria'
        : `${count} job${count > 1 ? 's' : ''} found`;
      
      resultsCount.textContent = message;
      
      // Also announce to screen readers
      accessibleUI.announceToScreenReader(message);
    });
  }
});

Color Contrast and Visual Design

Ensuring sufficient color contrast and visual clarity for users with visual impairments.

/* Accessible color system with WCAG AA compliance */
:root {
  /* Color palette with contrast ratios */
  --color-primary: #2563eb;        /* 4.5:1 on white */
  --color-primary-dark: #1d4ed8;   /* 7:1 on white */
  --color-secondary: #64748b;      /* 4.5:1 on white */
  --color-success: #059669;        /* 4.5:1 on white */
  --color-warning: #d97706;        /* 4.5:1 on white */
  --color-error: #dc2626;          /* 4.5:1 on white */
  
  /* Text colors */
  --text-primary: #111827;         /* 16:1 on white */
  --text-secondary: #374151;       /* 10:1 on white */
  --text-muted: #6b7280;          /* 4.5:1 on white */
  
  /* Background colors */
  --bg-primary: #ffffff;
  --bg-secondary: #f9fafb;
  --bg-accent: #f3f4f6;
  
  /* Focus indicators */
  --focus-ring: 0 0 0 3px rgba(37, 99, 235, 0.5);
  --focus-ring-error: 0 0 0 3px rgba(220, 38, 38, 0.5);
}

/* High contrast mode support */
@media (prefers-contrast: high) {
  :root {
    --color-primary: #0000ff;
    --color-error: #ff0000;
    --text-primary: #000000;
    --bg-primary: #ffffff;
  }
}

/* Reduced motion support */
@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;
  }
}

/* Focus management */
.focus-visible,
*:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: 4px;
}

/* Skip links */
.skip-link {
  position: absolute;
  top: -40px;
  left: 6px;
  background: var(--color-primary);
  color: white;
  padding: 8px;
  text-decoration: none;
  border-radius: 4px;
  z-index: 1000;
  transition: top 0.3s;
}

.skip-link:focus {
  top: 6px;
}

/* Screen reader only content */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Accessible form styling */
.form-group {
  margin-bottom: 1.5rem;
}

.form-group label {
  display: block;
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 0.5rem;
}

.form-group input,
.form-group select,
.form-group textarea {
  width: 100%;
  padding: 0.75rem;
  border: 2px solid #d1d5db;
  border-radius: 4px;
  font-size: 1rem;
  transition: border-color 0.2s, box-shadow 0.2s;
}

.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
  border-color: var(--color-primary);
  box-shadow: var(--focus-ring);
}

.form-group input.error,
.form-group select.error,
.form-group textarea.error {
  border-color: var(--color-error);
}

.form-group input.error:focus,
.form-group select.error:focus,
.form-group textarea.error:focus {
  box-shadow: var(--focus-ring-error);
}

.error-message {
  color: var(--color-error);
  font-size: 0.875rem;
  margin-top: 0.25rem;
  display: none;
}

.help-text {
  color: var(--text-muted);
  font-size: 0.875rem;
  margin-top: 0.25rem;
}

/* Accessible button styling */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.75rem 1.5rem;
  font-size: 1rem;
  font-weight: 500;
  text-decoration: none;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s;
  min-height: 44px; /* Minimum touch target size */
  min-width: 44px;
}

.btn:focus {
  box-shadow: var(--focus-ring);
}

.btn-primary {
  background-color: var(--color-primary);
  color: white;
}

.btn-primary:hover {
  background-color: var(--color-primary-dark);
}

.btn-primary:disabled {
  background-color: #9ca3af;
  cursor: not-allowed;
}

/* Accessible table styling */
.accessible-table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 1rem;
}

.accessible-table th,
.accessible-table td {
  padding: 0.75rem;
  text-align: left;
  border-bottom: 1px solid #e5e7eb;
}

.accessible-table th {
  background-color: var(--bg-accent);
  font-weight: 600;
  color: var(--text-primary);
}

.accessible-table caption {
  caption-side: top;
  text-align: left;
  font-weight: 600;
  margin-bottom: 0.5rem;
  color: var(--text-primary);
}

/* Accessible modal styling */
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0.3s;
}

.modal.modal-open {
  opacity: 1;
  visibility: visible;
}

.modal-content {
  background: white;
  border-radius: 8px;
  padding: 2rem;
  max-width: 500px;
  width: 90%;
  max-height: 90vh;
  overflow-y: auto;
  position: relative;
}

.modal-close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  background: none;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
  padding: 0.5rem;
  border-radius: 4px;
}

.modal-close:focus {
  box-shadow: var(--focus-ring);
}

/* Accessible dropdown styling */
.dropdown {
  position: relative;
  display: inline-block;
}

.dropdown-trigger {
  background: white;
  border: 2px solid #d1d5db;
  padding: 0.75rem 1rem;
  border-radius: 4px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.dropdown-trigger:focus {
  border-color: var(--color-primary);
  box-shadow: var(--focus-ring);
}

.dropdown-menu {
  position: absolute;
  top: 100%;
  left: 0;
  background: white;
  border: 2px solid #d1d5db;
  border-radius: 4px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  min-width: 200px;
  z-index: 100;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-10px);
  transition: all 0.2s;
}

.dropdown-menu.dropdown-open {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.dropdown-menu [role="menuitem"] {
  display: block;
  width: 100%;
  padding: 0.75rem 1rem;
  text-align: left;
  border: none;
  background: none;
  cursor: pointer;
  text-decoration: none;
  color: var(--text-primary);
}

.dropdown-menu [role="menuitem"]:focus,
.dropdown-menu [role="menuitem"]:hover {
  background-color: var(--bg-accent);
}

/* Accessible tab styling */
.tab-container {
  border: 1px solid #d1d5db;
  border-radius: 4px;
}

.tab-list {
  display: flex;
  background-color: var(--bg-accent);
  border-bottom: 1px solid #d1d5db;
  margin: 0;
  padding: 0;
  list-style: none;
}

.tab {
  background: none;
  border: none;
  padding: 1rem 1.5rem;
  cursor: pointer;
  border-bottom: 3px solid transparent;
  color: var(--text-secondary);
  font-weight: 500;
}

.tab[aria-selected="true"] {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
  background-color: white;
}

.tab:focus {
  box-shadow: var(--focus-ring);
}

.tab-panel {
  padding: 2rem;
}

Testing and Validation

Comprehensive accessibility testing ensures compliance and usability across different assistive technologies.

// Automated accessibility testing utilities
class AccessibilityTester {
  constructor() {
    this.violations = [];
    this.warnings = [];
    
    this.init();
  }
  
  init() {
    this.runAutomatedTests();
    this.setupContinuousMonitoring();
  }
  
  runAutomatedTests() {
    // Test color contrast
    this.testColorContrast();
    
    // Test keyboard navigation
    this.testKeyboardNavigation();
    
    // Test ARIA implementation
    this.testARIAImplementation();
    
    // Test form accessibility
    this.testFormAccessibility();
    
    // Test heading structure
    this.testHeadingStructure();
    
    // Generate report
    this.generateReport();
  }
  
  testColorContrast() {
    const textElements = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, a, button, span, div');
    
    textElements.forEach(element => {
      const styles = getComputedStyle(element);
      const textColor = styles.color;
      const backgroundColor = styles.backgroundColor;
      
      if (textColor && backgroundColor && backgroundColor !== 'rgba(0, 0, 0, 0)') {
        const contrast = this.calculateContrastRatio(textColor, backgroundColor);
        const fontSize = parseFloat(styles.fontSize);
        const fontWeight = styles.fontWeight;
        
        const isLargeText = fontSize >= 18 || (fontSize >= 14 && (fontWeight === 'bold' || fontWeight >= 700));
        const requiredRatio = isLargeText ? 3 : 4.5;
        
        if (contrast < requiredRatio) {
          this.violations.push({
            type: 'color-contrast',
            element: element,
            message: `Insufficient color contrast: ${contrast.toFixed(2)}:1 (required: ${requiredRatio}:1)`,
            severity: 'error'
          });
        }
      }
    });
  }
  
  calculateContrastRatio(color1, color2) {
    // Simplified contrast calculation
    // In production, use a proper color contrast library
    const rgb1 = this.parseColor(color1);
    const rgb2 = this.parseColor(color2);
    
    const l1 = this.getLuminance(rgb1);
    const l2 = this.getLuminance(rgb2);
    
    const lighter = Math.max(l1, l2);
    const darker = Math.min(l1, l2);
    
    return (lighter + 0.05) / (darker + 0.05);
  }
  
  parseColor(color) {
    // Simplified color parsing
    const div = document.createElement('div');
    div.style.color = color;
    document.body.appendChild(div);
    
    const computedColor = getComputedStyle(div).color;
    document.body.removeChild(div);
    
    const match = computedColor.match(/rgb\((\d+), (\d+), (\d+)\)/);
    if (match) {
      return {
        r: parseInt(match[1]),
        g: parseInt(match[2]),
        b: parseInt(match[3])
      };
    }
    
    return { r: 0, g: 0, b: 0 };
  }
  
  getLuminance(rgb) {
    const { r, g, b } = rgb;
    
    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;
  }
  
  testKeyboardNavigation() {
    const focusableElements = document.querySelectorAll(`
      a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])
    `);
    
    focusableElements.forEach(element => {
      // Check if element is visible
      const rect = element.getBoundingClientRect();
      const styles = getComputedStyle(element);
      
      if (rect.width === 0 || rect.height === 0 || styles.visibility === 'hidden') {
        this.warnings.push({
          type: 'keyboard-navigation',
          element: element,
          message: 'Focusable element is not visible',
          severity: 'warning'
        });
      }
      
      // Check for focus indicators
      if (!this.hasFocusIndicator(element)) {
        this.violations.push({
          type: 'keyboard-navigation',
          element: element,
          message: 'Element lacks visible focus indicator',
          severity: 'error'
        });
      }
    });
  }
  
  hasFocusIndicator(element) {
    // Simulate focus and check for visual changes
    const originalOutline = element.style.outline;
    const originalBoxShadow = element.style.boxShadow;
    
    element.focus();
    
    const focusedStyles = getComputedStyle(element);
    const hasOutline = focusedStyles.outline !== 'none';
    const hasBoxShadow = focusedStyles.boxShadow !== 'none';
    const hasVisibleFocus = hasOutline || hasBoxShadow;
    
    element.blur();
    
    return hasVisibleFocus;
  }
  
  testARIAImplementation() {
    // Test for missing ARIA labels
    const interactiveElements = document.querySelectorAll('button, a, input, select, textarea');
    
    interactiveElements.forEach(element => {
      const hasLabel = element.getAttribute('aria-label') ||
                      element.getAttribute('aria-labelledby') ||
                      element.textContent.trim() ||
                      (element.tagName === 'INPUT' && document.querySelector(`label[for="${element.id}"]`));
      
      if (!hasLabel) {
        this.violations.push({
          type: 'aria-implementation',
          element: element,
          message: 'Interactive element lacks accessible name',
          severity: 'error'
        });
      }
    });
    
    // Test for invalid ARIA attributes
    const elementsWithARIA = document.querySelectorAll('[aria-expanded], [aria-selected], [aria-checked]');
    
    elementsWithARIA.forEach(element => {
      const ariaExpanded = element.getAttribute('aria-expanded');
      const ariaSelected = element.getAttribute('aria-selected');
      const ariaChecked = element.getAttribute('aria-checked');
      
      if (ariaExpanded && !['true', 'false'].includes(ariaExpanded)) {
        this.violations.push({
          type: 'aria-implementation',
          element: element,
          message: 'Invalid aria-expanded value',
          severity: 'error'
        });
      }
      
      if (ariaSelected && !['true', 'false'].includes(ariaSelected)) {
        this.violations.push({
          type: 'aria-implementation',
          element: element,
          message: 'Invalid aria-selected value',
          severity: 'error'
        });
      }
      
      if (ariaChecked && !['true', 'false', 'mixed'].includes(ariaChecked)) {
        this.violations.push({
          type: 'aria-implementation',
          element: element,
          message: 'Invalid aria-checked value',
          severity: 'error'
        });
      }
    });
  }
  
  testFormAccessibility() {
    const forms = document.querySelectorAll('form');
    
    forms.forEach(form => {
      const inputs = form.querySelectorAll('input, select, textarea');
      
      inputs.forEach(input => {
        // Check for labels
        const hasLabel = document.querySelector(`label[for="${input.id}"]`) ||
                        input.getAttribute('aria-label') ||
                        input.getAttribute('aria-labelledby');
        
        if (!hasLabel) {
          this.violations.push({
            type: 'form-accessibility',
            element: input,
            message: 'Form input lacks associated label',
            severity: 'error'
          });
        }
        
        // Check required fields
        if (input.hasAttribute('required')) {
          const hasRequiredIndicator = input.getAttribute('aria-required') === 'true' ||
                                     input.closest('.form-group')?.querySelector('.required-indicator');
          
          if (!hasRequiredIndicator) {
            this.warnings.push({
              type: 'form-accessibility',
              element: input,
              message: 'Required field lacks clear indication',
              severity: 'warning'
            });
          }
        }
      });
    });
  }
  
  testHeadingStructure() {
    const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
    let previousLevel = 0;
    
    headings.forEach(heading => {
      const currentLevel = parseInt(heading.tagName.charAt(1));
      
      if (currentLevel > previousLevel + 1) {
        this.violations.push({
          type: 'heading-structure',
          element: heading,
          message: `Heading level skipped from h${previousLevel} to h${currentLevel}`,
          severity: 'error'
        });
      }
      
      previousLevel = currentLevel;
    });
    
    // Check for multiple h1 elements
    const h1Elements = document.querySelectorAll('h1');
    if (h1Elements.length > 1) {
      this.warnings.push({
        type: 'heading-structure',
        element: h1Elements[1],
        message: 'Multiple h1 elements found on page',
        severity: 'warning'
      });
    }
  }
  
  setupContinuousMonitoring() {
    // Monitor DOM changes for new accessibility issues
    const observer = new MutationObserver((mutations) => {
      mutations.forEach(mutation => {
        if (mutation.type === 'childList') {
          mutation.addedNodes.forEach(node => {
            if (node.nodeType === Node.ELEMENT_NODE) {
              this.testNewElement(node);
            }
          });
        }
      });
    });
    
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }
  
  testNewElement(element) {
    // Run accessibility tests on newly added elements
    if (element.matches('button, a, input, select, textarea')) {
      this.testElementAccessibility(element);
    }
  }
  
  testElementAccessibility(element) {
    // Quick accessibility check for individual elements
    const hasLabel = element.getAttribute('aria-label') ||
                    element.getAttribute('aria-labelledby') ||
                    element.textContent.trim();
    
    if (!hasLabel) {
      console.warn('New interactive element lacks accessible name:', element);
    }
  }
  
  generateReport() {
    const totalIssues = this.violations.length + this.warnings.length;
    
    console.group(`Accessibility Report - ${totalIssues} issues found`);
    
    if (this.violations.length > 0) {
      console.group(`Violations (${this.violations.length})`);
      this.violations.forEach(violation => {
        console.error(violation.message, violation.element);
      });
      console.groupEnd();
    }
    
    if (this.warnings.length > 0) {
      console.group(`Warnings (${this.warnings.length})`);
      this.warnings.forEach(warning => {
        console.warn(warning.message, warning.element);
      });
      console.groupEnd();
    }
    
    console.groupEnd();
    
    // Send report to analytics
    this.sendReportToAnalytics();
  }
  
  sendReportToAnalytics() {
    fetch('/api/accessibility/report', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        url: window.location.href,
        violations: this.violations.length,
        warnings: this.warnings.length,
        timestamp: Date.now()
      })
    }).catch(() => {
      // Ignore analytics failures
    });
  }
}

// Initialize accessibility testing
const accessibilityTester = new AccessibilityTester();

Conclusion

Web accessibility is not just about compliance—it's about creating inclusive experiences that work for everyone. By implementing proper semantic HTML, ARIA attributes, keyboard navigation, color contrast, and comprehensive testing, developers can build applications that are truly accessible to all users.

The key to successful accessibility implementation lies in considering accessibility from the beginning of the design process, not as an afterthought. Regular testing with real users, including those who use assistive technologies, provides invaluable insights for creating better experiences.

At Custom Logic, we prioritize accessibility in all our web development projects, ensuring that platforms like Custom Logic's website are usable by everyone. Our commitment to inclusive design reflects our belief that technology should empower all users, regardless of their abilities.

For businesses looking to improve their web accessibility and ensure WCAG compliance, Custom Logic provides comprehensive accessibility consulting and development services. We help organizations create inclusive digital experiences that serve all users while meeting legal requirements and demonstrating social responsibility.