# Accessible Design Patterns Library Reference implementations and best practices for common UI patterns with accessibility built-in. ## Navigation Patterns ### Skip Links **Purpose:** Allow keyboard users to bypass repetitive navigation and jump to main content. **HTML:** ```html Skip to main content ``` **CSS:** ```css .skip-link { position: absolute; top: -40px; left: 0; background: #000; color: #fff; padding: 8px; z-index: 100; } .skip-link:focus { top: 0; } ``` **Accessibility:** - Must be first focusable element - Visible on keyboard focus - Jumps to main content area with `id="main-content"` ### Responsive Navigation Menu **HTML:** ```html Menu Home About Contact ``` **JavaScript:** ```javascript const menuToggle = document.querySelector('.menu-toggle'); const menu = document.querySelector('#main-menu'); menuToggle.addEventListener('click', () => { const isExpanded = menuToggle.getAttribute('aria-expanded') === 'true'; menuToggle.setAttribute('aria-expanded', !isExpanded); menu.hidden = isExpanded; }); // Close on Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !menu.hidden) { menuToggle.setAttribute('aria-expanded', 'false'); menu.hidden = true; menuToggle.focus(); } }); ``` **Accessibility:** - `aria-label` on nav for multiple navigation regions - `aria-expanded` indicates menu state - `aria-current="page"` for current page - Escape key closes menu - Focus returns to toggle button when closed ### Breadcrumbs **HTML:** ```html Home Products Shoes Running Shoes ``` **CSS:** ```css .breadcrumb { display: flex; gap: 0.5rem; list-style: none; } .breadcrumb li:not(:last-child)::after { content: '/'; margin-left: 0.5rem; color: #666; } .breadcrumb [aria-current="page"] { font-weight: bold; color: #333; } ``` **Accessibility:** - Use `` with descriptive `aria-label` - Use ordered list `` for hierarchy - `aria-current="page"` for current location - Last item is not a link ## Form Patterns ### Accessible Form Input **HTML:** ```html Email address * We'll never share your email with anyone else. Please enter a valid email address. ``` **CSS:** ```css .form-field { margin-bottom: 1.5rem; } label { display: block; margin-bottom: 0.5rem; font-weight: 500; } input { display: block; width: 100%; padding: 0.5rem; border: 2px solid #ccc; border-radius: 4px; font-size: 1rem; } input:focus { outline: 2px solid #0066cc; outline-offset: 2px; border-color: #0066cc; } input[aria-invalid="true"] { border-color: #d32f2f; } .hint { display: block; margin-top: 0.25rem; font-size: 0.875rem; color: #666; } .error { display: block; margin-top: 0.25rem; font-size: 0.875rem; color: #d32f2f; } ``` **JavaScript:** ```javascript const emailInput = document.getElementById('email'); const emailError = document.getElementById('email-error'); emailInput.addEventListener('blur', () => { if (!emailInput.validity.valid) { emailInput.setAttribute('aria-invalid', 'true'); emailError.hidden = false; } else { emailInput.removeAttribute('aria-invalid'); emailError.hidden = true; } }); ``` **Accessibility:** - Label explicitly associated with input via `for`/`id` - Required indicator in label (not just asterisk) - `autocomplete` attribute for auto-fill - `aria-describedby` links to hint and error text - `aria-invalid` when validation fails - Error message has `role="alert"` for announcement ### Radio Button Group **HTML:** ```html Choose your shipping method Standard (3-5 business days) Express (1-2 business days) ``` **Accessibility:** - `` groups related radio buttons - `` provides group label - Each radio has explicit label - Keyboard navigation with arrow keys (native behavior) ### Toggle Switch **HTML:** ```html Enable notifications ``` **CSS:** ```css .toggle-switch { display: flex; align-items: center; gap: 0.75rem; } .toggle-switch input[type="checkbox"] { appearance: none; position: relative; width: 44px; height: 24px; background: #ccc; border-radius: 12px; cursor: pointer; transition: background 0.3s; } .toggle-switch input[type="checkbox"]::before { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background: white; border-radius: 50%; transition: transform 0.3s; } .toggle-switch input[type="checkbox"]:checked { background: #0066cc; } .toggle-switch input[type="checkbox"]:checked::before { transform: translateX(20px); } .toggle-switch input[type="checkbox"]:focus { outline: 2px solid #0066cc; outline-offset: 2px; } ``` **Accessibility:** - `role="switch"` indicates toggle behavior - `aria-checked` reflects state - Minimum 44x44px touch target - Clear focus indicator - Label describes purpose ## Modal Patterns ### Accessible Modal Dialog **HTML:** ```html Open Dialog Confirm Action Are you sure you want to delete this item? This action cannot be undone. Delete Cancel × ``` **CSS:** ```css .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; } .modal-content { position: relative; background: white; padding: 2rem; border-radius: 8px; max-width: 500px; width: 90%; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .close-button { position: absolute; top: 1rem; right: 1rem; background: none; border: none; font-size: 2rem; cursor: pointer; padding: 0; width: 44px; height: 44px; } ``` **JavaScript:** ```javascript const modal = document.getElementById('modal'); const openBtn = document.getElementById('open-modal'); const closeBtn = document.getElementById('close-modal'); const cancelBtn = document.getElementById('cancel-btn'); let lastFocusedElement; // Open modal openBtn.addEventListener('click', () => { lastFocusedElement = document.activeElement; modal.hidden = false; // Focus first focusable element const firstFocusable = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); firstFocusable?.focus(); // Trap focus trapFocus(modal); }); // Close modal function closeModal() { modal.hidden = true; lastFocusedElement?.focus(); } closeBtn.addEventListener('click', closeModal); cancelBtn.addEventListener('click', closeModal); // Close on Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !modal.hidden) { closeModal(); } }); // Close on overlay click modal.addEventListener('click', (e) => { if (e.target === modal) { closeModal(); } }); // Focus trap function trapFocus(element) { const focusableElements = element.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstFocusable = focusableElements[0]; const lastFocusable = focusableElements[focusableElements.length - 1]; element.addEventListener('keydown', (e) => { if (e.key !== 'Tab') return; if (e.shiftKey) { if (document.activeElement === firstFocusable) { lastFocusable.focus(); e.preventDefault(); } } else { if (document.activeElement === lastFocusable) { firstFocusable.focus(); e.preventDefault(); } } }); } ``` **Accessibility:** - `role="dialog"` and `aria-modal="true"` - `aria-labelledby` references dialog title - `aria-describedby` references dialog description - Focus moved to modal when opened - Focus trapped within modal - Escape key closes modal - Focus returned to trigger on close - Close button has `aria-label` ## Button Patterns ### Icon Button **HTML:** ```html ``` **Accessibility:** - `aria-label` provides accessible name - `aria-hidden="true"` hides icon from screen readers - `focusable="false"` prevents icon from receiving focus ### Loading Button **HTML:** ```html Submit ``` **JavaScript:** ```javascript const submitBtn = document.getElementById('submit-btn'); const buttonText = submitBtn.querySelector('.button-text'); const spinner = submitBtn.querySelector('.spinner'); submitBtn.addEventListener('click', async () => { // Start loading submitBtn.setAttribute('aria-busy', 'true'); submitBtn.disabled = true; buttonText.textContent = 'Submitting...'; spinner.hidden = false; // Simulate async operation await fetch('/api/submit'); // End loading submitBtn.setAttribute('aria-busy', 'false'); submitBtn.disabled = false; buttonText.textContent = 'Submit'; spinner.hidden = true; }); ``` **Accessibility:** - `aria-busy` indicates loading state - Button text changes to describe current state - Button disabled during loading - Spinner has `aria-hidden="true"` ## Dropdown/Select Patterns ### Custom Dropdown (Combobox) **HTML:** ```html Choose a fruit Apple Banana Cherry ``` **JavaScript:** ```javascript const combobox = document.getElementById('combo-input'); const listbox = document.getElementById('combo-listbox'); const options = listbox.querySelectorAll('[role="option"]'); let activeIndex = -1; // Open listbox combobox.addEventListener('focus', () => { combobox.setAttribute('aria-expanded', 'true'); listbox.hidden = false; }); // Keyboard navigation combobox.addEventListener('keydown', (e) => { switch(e.key) { case 'ArrowDown': e.preventDefault(); activeIndex = Math.min(activeIndex + 1, options.length - 1); updateActiveOption(); break; case 'ArrowUp': e.preventDefault(); activeIndex = Math.max(activeIndex - 1, 0); updateActiveOption(); break; case 'Enter': if (activeIndex >= 0) { selectOption(options[activeIndex]); } break; case 'Escape': closeListbox(); break; } }); function updateActiveOption() { options.forEach((option, index) => { if (index === activeIndex) { option.setAttribute('aria-selected', 'true'); combobox.setAttribute('aria-activedescendant', option.id); option.scrollIntoView({ block: 'nearest' }); } else { option.removeAttribute('aria-selected'); } }); } function selectOption(option) { combobox.value = option.textContent; closeListbox(); } function closeListbox() { combobox.setAttribute('aria-expanded', 'false'); listbox.hidden = true; activeIndex = -1; } ``` **Accessibility:** - `role="combobox"` for input - `aria-expanded` indicates dropdown state - `aria-controls` links to listbox - `aria-activedescendant` tracks active option - Arrow keys navigate options - Enter selects option - Escape closes dropdown ## Alert Patterns ### Success Message **HTML:** ```html Your changes have been saved successfully. ``` ### Error Message **HTML:** ```html An error occurred. Please try again. ``` **Accessibility:** - `role="status"` for non-critical updates - `role="alert"` for critical messages - `aria-live="polite"` waits for pause - `aria-live="assertive"` interrupts immediately - Icons are decorative (`aria-hidden`) ## Accordion Pattern **HTML:** ```html Section 1 + Content for section 1. ``` **JavaScript:** ```javascript const triggers = document.querySelectorAll('.accordion-trigger'); triggers.forEach(trigger => { trigger.addEventListener('click', () => { const expanded = trigger.getAttribute('aria-expanded') === 'true'; const panel = document.getElementById(trigger.getAttribute('aria-controls')); const icon = trigger.querySelector('.accordion-icon'); trigger.setAttribute('aria-expanded', !expanded); panel.hidden = expanded; icon.textContent = expanded ? '+' : '−'; }); }); ``` **Accessibility:** - Button wraps heading text - `aria-expanded` indicates state - `aria-controls` links to panel - Panel has `role="region"` - `aria-labelledby` links panel to heading - Icon is decorative ## Best Practices Summary ### General Principles 1. Use semantic HTML first 2. Add ARIA only when semantic HTML isn't sufficient 3. Ensure keyboard accessibility for all interactions 4. Provide visible focus indicators 5. Test with actual assistive technologies 6. Don't rely on color alone 7. Maintain proper heading hierarchy 8. Provide alternative text for images 9. Ensure sufficient color contrast 10. Support screen reader announcements ### Common Mistakes to Avoid - ❌ Removing focus outlines without replacement - ❌ Using `` or `` for buttons - ❌ Placeholder text as labels - ❌ Click handlers on non-interactive elements - ❌ Missing alt text on images - ❌ Poor color contrast - ❌ Keyboard traps - ❌ Auto-playing audio/video - ❌ Time limits without extensions - ❌ Unlabeled form controls ### Testing Checklist - ✅ Keyboard-only navigation - ✅ Screen reader testing - ✅ Automated accessibility scans - ✅ Color contrast verification - ✅ Zoom to 200% - ✅ Focus indicator visibility - ✅ Touch target sizes - ✅ Error message clarity - ✅ Form label associations - ✅ Semantic HTML validation
Are you sure you want to delete this item? This action cannot be undone.
Content for section 1.