<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script> <script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script> <a href="#main-content" class="skip-link">Skip to main content</a> <div class="container" id="main-content"> <h1>JavaScript Accessibility Demo</h1> <p class="subtitle">Best practices for building accessible web applications</p> <!-- 1. Keyboard Navigation --> <div class="demo-section"> <h2> <ion-icon name="keypad-outline"></ion-icon> Keyboard Navigation </h2> <p class="feature-description">Custom button with full keyboard support (Enter/Space)</p> <button class="custom-btn" id="keyboardBtn"> <ion-icon name="hand-left-outline"></ion-icon> Click or Press Enter/Space </button> </div> <!-- 2. Form Validation --> <div class="demo-section"> <h2> <ion-icon name="checkmark-circle-outline"></ion-icon> Accessible Form Validation </h2> <p class="feature-description">Real-time validation with ARIA attributes</p> <form id="accessibleForm"> <div class="form-group"> <label for="username">Username (required)</label> <input type="text" id="username" name="username" aria-required="true" aria-describedby="username-error" aria-invalid="false" > <div id="username-error" class="error-message" role="alert"> <ion-icon name="alert-circle-outline"></ion-icon> <span>Username must be at least 3 characters</span> </div> </div> <div class="form-group"> <label for="email">Email (required)</label> <input type="email" id="email" name="email" aria-required="true" aria-describedby="email-error" aria-invalid="false" > <div id="email-error" class="error-message" role="alert"> <ion-icon name="alert-circle-outline"></ion-icon> <span>Please enter a valid email address</span> </div> </div> <button type="submit" class="custom-btn"> <ion-icon name="paper-plane-outline"></ion-icon> Submit Form </button> </form> </div> <!-- 3. Live Region --> <div class="demo-section"> <h2> <ion-icon name="notifications-outline"></ion-icon> ARIA Live Regions </h2> <p class="feature-description">Dynamic content announcements for screen readers</p> <div class="live-region-demo"> <button class="custom-btn" id="liveRegionBtn"> <ion-icon name="refresh-outline"></ion-icon> Update Status </button> <div id="statusMessage" class="status-message" role="status" aria-live="polite"> <ion-icon name="checkmark-circle"></ion-icon> <span>Status updated successfully!</span> </div> </div> </div> <!-- 4. Focus Management --> <div class="demo-section"> <h2> <ion-icon name="scan-outline"></ion-icon> Focus Trap (Modal) </h2> <p class="feature-description">Proper focus management in modal dialogs</p> <button class="custom-btn" id="openModalBtn"> <ion-icon name="open-outline"></ion-icon> Open Modal </button> </div> </div> <!-- Modal --> <div class="modal" id="modal" role="dialog" aria-labelledby="modal-title" aria-modal="true"> <div class="modal-content"> <h3 id="modal-title">Accessible Modal</h3> <p>Focus is trapped within this modal. Press Tab to cycle through focusable elements, or Escape to close.</p> <div class="modal-actions"> <button class="custom-btn" id="modalAction"> <ion-icon name="checkmark-outline"></ion-icon> Confirm </button> <button class="btn-secondary" id="closeModalBtn"> <ion-icon name="close-outline"></ion-icon> Close </button> </div> </div> </div>
* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #f5f5f5; padding: 40px 20px; line-height: 1.6; } .container { max-width: 800px; margin: 0 auto; background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } h1 { color: #2c3e50; margin-bottom: 10px; font-size: 28px; } .subtitle { color: #7f8c8d; margin-bottom: 30px; font-size: 14px; } .demo-section { margin-bottom: 40px; padding-bottom: 40px; border-bottom: 1px solid #ecf0f1; } .demo-section:last-child { border-bottom: none; } h2 { color: #34495e; margin-bottom: 15px; font-size: 20px; display: flex; align-items: center; gap: 10px; } .feature-description { color: #7f8c8d; margin-bottom: 20px; font-size: 14px; } /* Custom Button with Keyboard Support */ .custom-btn { background: #3498db; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; transition: all 0.2s; display: inline-flex; align-items: center; gap: 8px; } .custom-btn:hover { background: #2980b9; transform: translateY(-1px); } .custom-btn:focus { outline: 3px solid #3498db; outline-offset: 2px; } .custom-btn:active { transform: translateY(0); } /* Skip Link */ .skip-link { position: absolute; top: -40px; left: 0; background: #2c3e50; color: white; padding: 8px 16px; text-decoration: none; border-radius: 0 0 4px 0; } .skip-link:focus { top: 0; } /* Accessible Form */ .form-group { margin-bottom: 20px; } label { display: block; margin-bottom: 6px; color: #2c3e50; font-weight: 500; } input[type="text"], input[type="email"] { width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 4px; font-size: 16px; transition: border-color 0.2s; } input:focus { outline: none; border-color: #3498db; } .error-message { color: #e74c3c; font-size: 14px; margin-top: 5px; display: none; } .error-message.show { display: flex; align-items: center; gap: 5px; } /* Live Region */ .live-region-demo { background: #ecf0f1; padding: 20px; border-radius: 6px; } .status-message { background: #27ae60; color: white; padding: 12px; border-radius: 4px; margin-top: 10px; display: none; align-items: center; gap: 8px; } .status-message.show { display: flex; } /* Focus Trap Demo */ .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); justify-content: center; align-items: center; z-index: 1000; } .modal.show { display: flex; } .modal-content { background: white; padding: 30px; border-radius: 8px; max-width: 400px; width: 90%; } .modal h3 { color: #2c3e50; margin-bottom: 15px; } .modal-actions { display: flex; gap: 10px; margin-top: 20px; } .btn-secondary { background: #95a5a6; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; } .btn-secondary:hover { background: #7f8c8d; } .btn-secondary:focus { outline: 3px solid #95a5a6; outline-offset: 2px; }
// 1. Keyboard Navigation const keyboardBtn = document.getElementById('keyboardBtn'); keyboardBtn.addEventListener('click', () => { alert('Button activated! This works with mouse, keyboard, and assistive technologies.'); }); // 2. Form Validation with ARIA const form = document.getElementById('accessibleForm'); const username = document.getElementById('username'); const email = document.getElementById('email'); const usernameError = document.getElementById('username-error'); const emailError = document.getElementById('email-error'); function validateUsername() { if (username.value.length < 3) { username.setAttribute('aria-invalid', 'true'); usernameError.classList.add('show'); return false; } else { username.setAttribute('aria-invalid', 'false'); usernameError.classList.remove('show'); return true; } } function validateEmail() { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email.value)) { email.setAttribute('aria-invalid', 'true'); emailError.classList.add('show'); return false; } else { email.setAttribute('aria-invalid', 'false'); emailError.classList.remove('show'); return true; } } username.addEventListener('blur', validateUsername); email.addEventListener('blur', validateEmail); form.addEventListener('submit', (e) => { e.preventDefault(); const isUsernameValid = validateUsername(); const isEmailValid = validateEmail(); if (isUsernameValid && isEmailValid) { alert('Form submitted successfully!'); form.reset(); } else { const firstInvalid = form.querySelector('[aria-invalid="true"]'); if (firstInvalid) firstInvalid.focus(); } }); // 3. Live Region const liveRegionBtn = document.getElementById('liveRegionBtn'); const statusMessage = document.getElementById('statusMessage'); liveRegionBtn.addEventListener('click', () => { statusMessage.classList.add('show'); setTimeout(() => { statusMessage.classList.remove('show'); }, 3000); }); // 4. Focus Trap in Modal const modal = document.getElementById('modal'); const openModalBtn = document.getElementById('openModalBtn'); const closeModalBtn = document.getElementById('closeModalBtn'); const modalAction = document.getElementById('modalAction'); let lastFocusedElement; function openModal() { lastFocusedElement = document.activeElement; modal.classList.add('show'); closeModalBtn.focus(); document.addEventListener('keydown', handleModalKeydown); } function closeModal() { modal.classList.remove('show'); document.removeEventListener('keydown', handleModalKeydown); if (lastFocusedElement) lastFocusedElement.focus(); } function handleModalKeydown(e) { if (e.key === 'Escape') { closeModal(); return; } if (e.key === 'Tab') { const focusableElements = modal.querySelectorAll('button'); const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; if (e.shiftKey && document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } else if (!e.shiftKey && document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } } openModalBtn.addEventListener('click', openModal); closeModalBtn.addEventListener('click', closeModal); modalAction.addEventListener('click', () => { alert('Action confirmed!'); closeModal(); }); modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });
Login to leave a comment
No comments yet. Be the first!
View Project
No comments yet. Be the first!