HTML Interactive Components Guide

Interactive components enhance user experience by providing dynamic functionality without requiring page reloads. This guide covers how to build common interactive UI components using HTML, CSS, and JavaScript.

1. Accordion Components

Accordions allow you to display a large amount of content in a limited space by showing/hiding sections as needed.

<!-- HTML Structure --> <div class="accordion-container"> <div class="accordion-item"> <h3 class="accordion-header" id="accordion-1">Section 1</h3> <div class="accordion-content" id="accordion-content-1" aria-labelledby="accordion-1"> <p>Content for section 1...</p> </div> </div> <div class="accordion-item"> <h3 class="accordion-header" id="accordion-2">Section 2</h3> <div class="accordion-content" id="accordion-content-2" aria-labelledby="accordion-2"> <p>Content for section 2...</p> </div> </div> </div> <!-- CSS --> <style> .accordion-header { background: #f0f0f0; padding: 15px; cursor: pointer; margin: 0; border: 1px solid #ddd; } .accordion-content { display: none; padding: 15px; border: 1px solid #ddd; border-top: none; } .accordion-content.active { display: block; } </style> <!-- JavaScript --> <script> document.querySelectorAll('.accordion-header').forEach(header => { header.addEventListener('click', () => { // Get the target content ID from header ID const contentId = header.id.replace('accordion-', 'accordion-content-'); const content = document.getElementById(contentId); // Toggle the active class content.classList.toggle('active'); // Set ARIA attributes const isExpanded = content.classList.contains('active'); header.setAttribute('aria-expanded', isExpanded); }); }); // Initialize ARIA attributes document.querySelectorAll('.accordion-header').forEach(header => { const contentId = header.id.replace('accordion-', 'accordion-content-'); const content = document.getElementById(contentId); header.setAttribute('aria-expanded', 'false'); header.setAttribute('role', 'button'); content.setAttribute('role', 'region'); }); </script>

Section 1

This is the content for section 1. It can contain any HTML elements like paragraphs, lists, images, etc.

The content is hidden by default and revealed when the header is clicked.

Section 2

This is the content for section 2. Accordions are commonly used in FAQs, product details, and other sections where you want to save space.

Accessibility Considerations

Best Practice: For simple accordions, the "disclosure" pattern (using the HTML <details> and <summary> elements) provides built-in functionality without requiring JavaScript:
<details> <summary>Section Title</summary> <p>Content for this section...</p> </details>

2. Tab Components

Tabs allow users to navigate between sections of content within the same page, showing only one section at a time.

<!-- HTML Structure --> <div class="tab-container"> <div role="tablist" aria-label="Content tabs"> <button id="tab-1" class="tab-button active" role="tab" aria-selected="true" aria-controls="panel-1" > Tab 1 </button> <button id="tab-2" class="tab-button" role="tab" aria-selected="false" aria-controls="panel-2" > Tab 2 </button> <button id="tab-3" class="tab-button" role="tab" aria-selected="false" aria-controls="panel-3" > Tab 3 </button> </div> <div id="panel-1" class="tab-panel active" role="tabpanel" aria-labelledby="tab-1" > <h3>Content for Tab 1</h3> <p>This is the content for the first tab.</p> </div> <div id="panel-2" class="tab-panel" role="tabpanel" aria-labelledby="tab-2" hidden > <h3>Content for Tab 2</h3> <p>This is the content for the second tab.</p> </div> <div id="panel-3" class="tab-panel" role="tabpanel" aria-labelledby="tab-3" hidden > <h3>Content for Tab 3</h3> <p>This is the content for the third tab.</p> </div> </div> <!-- CSS --> <style> .tab-container { margin: 20px 0; } [role="tablist"] { display: flex; list-style: none; padding: 0; margin: 0; border-bottom: 1px solid #ddd; } .tab-button { padding: 10px 15px; background: #f0f0f0; border: 1px solid #ddd; border-bottom: none; border-radius: 4px 4px 0 0; margin-right: 5px; cursor: pointer; } .tab-button.active { background: white; border-bottom: 1px solid white; margin-bottom: -1px; } .tab-panel { padding: 15px; border: 1px solid #ddd; border-top: none; } .tab-panel[hidden] { display: none; } </style> <!-- JavaScript --> <script> // Get all tab buttons and panels const tabs = document.querySelectorAll('[role="tab"]'); const panels = document.querySelectorAll('[role="tabpanel"]'); // Add click event to each tab tabs.forEach(tab => { tab.addEventListener('click', () => { // Deactivate all tabs tabs.forEach(t => { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); }); // Hide all panels panels.forEach(panel => { panel.hidden = true; }); // Activate clicked tab tab.classList.add('active'); tab.setAttribute('aria-selected', 'true'); // Show the associated panel const panelId = tab.getAttribute('aria-controls'); const panel = document.getElementById(panelId); panel.hidden = false; }); // Add keyboard navigation tab.addEventListener('keydown', e => { // Current tab index const index = Array.from(tabs).indexOf(tab); let nextTab; switch(e.key) { case 'ArrowRight': nextTab = tabs[(index + 1) % tabs.length]; break; case 'ArrowLeft': nextTab = tabs[(index - 1 + tabs.length) % tabs.length]; break; default: return; } e.preventDefault(); nextTab.focus(); nextTab.click(); }); }); </script>

Content for Tab 1

This is the content for the first tab panel. It can contain any HTML elements.

Accessibility Considerations

3. Modal Dialogs

Modal dialogs display content in a layer above the page, temporarily disabling interaction with the page beneath.

<!-- HTML Structure --> <!-- Trigger button --> <button class="modal-trigger" aria-haspopup="dialog">Open Modal</button> <!-- Modal dialog --> <div id="modal" class="modal-container" role="dialog" aria-labelledby="modal-title" aria-modal="true" hidden > <div class="modal-content"> <button class="modal-close" aria-label="Close dialog">×</button> <h2 id="modal-title">Modal Title</h2> <div class="modal-body"> <p>This is the modal content.</p> </div> <div class="modal-footer"> <button class="modal-confirm">Confirm</button> <button class="modal-cancel">Cancel</button> </div> </div> </div> <!-- CSS --> <style> .modal-container { 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; } .modal-content { background-color: white; padding: 20px; border-radius: 5px; max-width: 500px; width: 90%; position: relative; } .modal-close { position: absolute; top: 10px; right: 10px; font-size: 24px; background: none; border: none; cursor: pointer; } .modal-footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } .modal-container[hidden] { display: none; } </style> <!-- JavaScript --> <script> // Get elements const modal = document.getElementById('modal'); const openButton = document.querySelector('.modal-trigger'); const closeButton = modal.querySelector('.modal-close'); const cancelButton = modal.querySelector('.modal-cancel'); const confirmButton = modal.querySelector('.modal-confirm'); // Store element that had focus before modal opened let previouslyFocused; // First and last focusable elements in the modal let firstFocusable; let lastFocusable; // Open modal function function openModal() { // Store the element that had focus previouslyFocused = document.activeElement; // Show the modal modal.hidden = false; // Find all focusable elements const focusableElements = modal.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); // Save first and last focusable elements firstFocusable = focusableElements[0]; lastFocusable = focusableElements[focusableElements.length - 1]; // Focus first element firstFocusable.focus(); // Trap focus inside modal document.addEventListener('keydown', handleKeyDown); } // Close modal function function closeModal() { // Hide modal modal.hidden = true; // Restore focus previouslyFocused.focus(); // Remove event listener document.removeEventListener('keydown', handleKeyDown); } // Handle keyboard events function handleKeyDown(e) { // Close on Escape if (e.key === 'Escape') { closeModal(); return; } // Trap focus if (e.key === 'Tab') { if (e.shiftKey) { // Shift + Tab if (document.activeElement === firstFocusable) { e.preventDefault(); lastFocusable.focus(); } } else { // Tab if (document.activeElement === lastFocusable) { e.preventDefault(); firstFocusable.focus(); } } } } // Event listeners openButton.addEventListener('click', openModal); closeButton.addEventListener('click', closeModal); cancelButton.addEventListener('click', closeModal); confirmButton.addEventListener('click', () => { // Handle confirmation console.log('Confirmed'); closeModal(); }); // Close when clicking outside the modal content modal.addEventListener('click', (e) => { if (e.target === modal) { closeModal(); } }); </script>

Accessibility Considerations

Native Dialog Element: Modern browsers support the <dialog> element, which provides many of these features natively:
<dialog id="my-dialog"> <h2>Dialog Title</h2> <p>Dialog content...</p> <button id="close-dialog">Close</button> </dialog> <button id="open-dialog">Open Dialog</button> <script> const dialog = document.getElementById('my-dialog'); const openBtn = document.getElementById('open-dialog'); const closeBtn = document.getElementById('close-dialog'); openBtn.addEventListener('click', () => { dialog.showModal(); }); closeBtn.addEventListener('click', () => { dialog.close(); }); </script>

4. Dropdown Menus

Dropdown menus display a list of options when a trigger element is activated. They're commonly used for navigation, settings, or actions.

<!-- HTML Structure --> <div class="dropdown-container"> <button id="dropdown-trigger" class="dropdown-trigger" aria-haspopup="true" aria-expanded="false" aria-controls="dropdown-menu" > Menu ▼ </button> <ul id="dropdown-menu" class="dropdown-menu" role="menu" aria-labelledby="dropdown-trigger" hidden > <li role="menuitem"><a href="#">Option 1</a></li> <li role="menuitem"><a href="#">Option 2</a></li> <li role="menuitem"><a href="#">Option 3</a></li> <li role="separator"><hr></li> <li role="menuitem"><a href="#">Option 4</a></li> </ul> </div> <!-- CSS --> <style> .dropdown-container { position: relative; display: inline-block; } .dropdown-trigger { padding: 8px 12px; background-color: #f0f0f0; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; } .dropdown-menu { position: absolute; top: 100%; left: 0; min-width: 160px; background-color: white; border: 1px solid #ddd; border-radius: 4px; padding: 5px 0; margin: 5px 0 0; list-style: none; box-shadow: 0 2px 5px rgba(0,0,0,0.1); z-index: 10; } .dropdown-menu[hidden] { display: none; } .dropdown-menu li a { display: block; padding: 8px 12px; text-decoration: none; color: #333; } .dropdown-menu li a:hover, .dropdown-menu li a:focus { background-color: #f5f5f5; } [role="separator"] { margin: 5px 0; } [role="separator"] hr { border: none; border-top: 1px solid #ddd; margin: 0; } </style> <!-- JavaScript --> <script> const trigger = document.getElementById('dropdown-trigger'); const menu = document.getElementById('dropdown-menu'); const menuItems = menu.querySelectorAll('[role="menuitem"] a'); // Toggle menu function toggleMenu() { const expanded = trigger.getAttribute('aria-expanded') === 'true'; if (expanded) { closeMenu(); } else { openMenu(); } } // Open menu function openMenu() { menu.hidden = false; trigger.setAttribute('aria-expanded', 'true'); // Focus first menu item menuItems[0].focus(); // Add event listeners document.addEventListener('click', handleOutsideClick); menu.addEventListener('keydown', handleMenuKeyDown); } // Close menu function closeMenu() { menu.hidden = true; trigger.setAttribute('aria-expanded', 'false'); // Remove event listeners document.removeEventListener('click', handleOutsideClick); menu.removeEventListener('keydown', handleMenuKeyDown); } // Handle clicks outside the menu function handleOutsideClick(e) { if (!menu.contains(e.target) && !trigger.contains(e.target)) { closeMenu(); } } // Handle keyboard navigation function handleMenuKeyDown(e) { const currentIndex = Array.from(menuItems).indexOf(document.activeElement); switch(e.key) { case 'ArrowDown': e.preventDefault(); if (currentIndex < menuItems.length - 1) { menuItems[currentIndex + 1].focus(); } break; case 'ArrowUp': e.preventDefault(); if (currentIndex > 0) { menuItems[currentIndex - 1].focus(); } break; case 'Escape': e.preventDefault(); closeMenu(); trigger.focus(); break; case 'Tab': closeMenu(); break; } } // Event listeners trigger.addEventListener('click', toggleMenu); // Handle key events on trigger trigger.addEventListener('keydown', (e) => { if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openMenu(); } }); // Handle selection menuItems.forEach(item => { item.addEventListener('click', () => { // Do something with the selected item console.log('Selected:', item.textContent); closeMenu(); }); }); </script>

Accessibility Considerations

5. Carousels / Sliders

Carousels (also called sliders) display a series of content items within a contained area, showing one or more items at a time with navigation controls.

<!-- HTML Structure --> <div class="carousel-container"> <h2 id="carousel-heading">Featured Products</h2> <div class="carousel" role="region" aria-roledescription="carousel" aria-labelledby="carousel-heading" > <ul class="carousel-items" role="list" > <li class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 4" > <img src="slide1.jpg" alt="Description of slide 1"> </li> <li class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 4" hidden > <img src="slide2.jpg" alt="Description of slide 2"> </li> <li class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 4" hidden > <img src="slide3.jpg" alt="Description of slide 3"> </li> <li class="carousel-item" role="group" aria-roledescription="slide" aria-label="4 of 4" hidden > <img src="slide4.jpg" alt="Description of slide 4"> </li> </ul> <div class="carousel-controls"> <button class="carousel-prev" aria-label="Previous slide" > <span aria-hidden="true">&laquo;</span> </button> <div class="carousel-indicators" role="tablist"> <button role="tab" aria-selected="true" aria-label="Slide 1" aria-controls="slide-1" ></button> <button role="tab" aria-selected="false" aria-label="Slide 2" aria-controls="slide-2" ></button> <button role="tab" aria-selected="false" aria-label="Slide 3" aria-controls="slide-3" ></button> <button role="tab" aria-selected="false" aria-label="Slide 4" aria-controls="slide-4" ></button> </div> <button class="carousel-next" aria-label="Next slide" > <span aria-hidden="true">&raquo;</span> </button> </div> </div> </div> <!-- CSS (simplified) --> <style> .carousel-container { max-width: 800px; margin: 0 auto; } .carousel { position: relative; overflow: hidden; } .carousel-items { list-style: none; padding: 0; margin: 0; width: 100%; height: 400px; } .carousel-item { position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; transition: opacity 0.3s ease; } .carousel-item.active { opacity: 1; z-index: 1; } .carousel-item img { width: 100%; height: 100%; object-fit: cover; } .carousel-controls { position: absolute; bottom: 20px; left: 0; right: 0; display: flex; justify-content: center; align-items: center; z-index: 2; } .carousel-prev, .carousel-next { background: rgba(0,0,0,0.5); color: white; border: none; border-radius: 50%; width: 40px; height: 40px; font-size: 18px; cursor: pointer; } .carousel-indicators { display: flex; margin: 0 10px; } .carousel-indicators button { width: 10px; height: 10px; border-radius: 50%; border: none; background: rgba(255,255,255,0.5); margin: 0 5px; cursor: pointer; } .carousel-indicators button[aria-selected="true"] { background: white; } </style> <!-- JavaScript (simplified) --> <script> // Get elements const slides = document.querySelectorAll('.carousel-item'); const prevButton = document.querySelector('.carousel-prev'); const nextButton = document.querySelector('.carousel-next'); const indicators = document.querySelectorAll('.carousel-indicators button'); // Track current slide let currentSlide = 0; const totalSlides = slides.length; // Function to update aria labels when slide changes function updateSlideLabels() { slides.forEach((slide, index) => { slide.setAttribute('aria-label', `${index + 1} of ${totalSlides}`); }); } // Initialize slide labels updateSlideLabels(); // Function to show a specific slide function showSlide(index) { // Handle index bounds if (index < 0) index = totalSlides - 1; if (index >= totalSlides) index = 0; // Update current slide index currentSlide = index; // Hide all slides slides.forEach((slide, i) => { slide.classList.remove('active'); slide.hidden = true; indicators[i].setAttribute('aria-selected', 'false'); }); // Show current slide slides[currentSlide].classList.add('active'); slides[currentSlide].hidden = false; indicators[currentSlide].setAttribute('aria-selected', 'true'); // Announce slide change to screen readers announceSlide(currentSlide); } // Function to announce slide change to screen readers function announceSlide(index) { // Create live region if not exists let liveRegion = document.getElementById('carousel-live'); if (!liveRegion) { liveRegion = document.createElement('div'); liveRegion.id = 'carousel-live'; liveRegion.setAttribute('aria-live', 'polite'); liveRegion.setAttribute('aria-atomic', 'true'); liveRegion.className = 'sr-only'; // Visually hidden document.body.appendChild(liveRegion); } // Announce current slide liveRegion.textContent = `Showing slide ${index + 1} of ${totalSlides}`; } // Event listeners for buttons prevButton.addEventListener('click', () => showSlide(currentSlide - 1)); nextButton.addEventListener('click', () => showSlide(currentSlide + 1)); // Event listeners for indicators indicators.forEach((indicator, index) => { indicator.addEventListener('click', () => showSlide(index)); }); // Auto-advance (optional) let autoplayId; function startAutoplay() { autoplayId = setInterval(() => { showSlide(currentSlide + 1); }, 5000); // Change slide every 5 seconds } function stopAutoplay() { clearInterval(autoplayId); } // Pause autoplay on hover/focus const carousel = document.querySelector('.carousel'); carousel.addEventListener('mouseenter', stopAutoplay); carousel.addEventListener('mouseleave', startAutoplay); carousel.addEventListener('focusin', stopAutoplay); carousel.addEventListener('focusout', startAutoplay); // Start autoplay startAutoplay(); // Show first slide initially showSlide(0); </script>

Accessibility Considerations

Caution: Carousels can present accessibility challenges. Consider these alternatives:
  • Static images with text descriptions
  • Tabbed interface where users can choose what content to view
  • Grid layout showing all items at once
  • Single featured item with a link to view more

6. Tooltips

Tooltips provide additional information when a user hovers over or focuses on an element.

<!-- HTML Structure --> <!-- Basic tooltip with aria-describedby --> <div class="tooltip-container"> <button id="info-button" aria-describedby="tooltip-1" > More Info </button> <div id="tooltip-1" role="tooltip" class="tooltip" > This is additional information <div class="tooltip-arrow"></div> </div> </div> <!-- CSS --> <style> .tooltip-container { position: relative; display: inline-block; } .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); padding: 8px 10px; background-color: #333; color: white; border-radius: 4px; font-size: 14px; white-space: nowrap; opacity: 0; visibility: hidden; transition: opacity 0.3s, visibility 0.3s; z-index: 10; } .tooltip-arrow { position: absolute; top: 100%; left: 50%; transform: translateX(-50%); border-width: 5px; border-style: solid; border-color: #333 transparent transparent transparent; } /* Show tooltip on hover/focus */ .tooltip-container:hover .tooltip, .tooltip-container:focus-within .tooltip { opacity: 1; visibility: visible; } </style> <!-- JavaScript for more complex tooltips --> <script> // For more complex behavior, like click-to-show tooltips // or dynamic positioning based on available space class Tooltip { constructor(element) { this.element = element; this.tooltip = document.getElementById( element.getAttribute('aria-describedby') ); this.position = this.tooltip.getAttribute('data-position') || 'top'; this.init(); } init() { // Toggle on click for mobile accessibility this.element.addEventListener('click', (e) => { e.preventDefault(); this.toggle(); }); // Show on hover for desktop this.element.addEventListener('mouseenter', () => { this.show(); }); this.element.addEventListener('mouseleave', () => { this.hide(); }); // Show on focus for keyboard users this.element.addEventListener('focus', () => { this.show(); }); this.element.addEventListener('blur', () => { this.hide(); }); // Close when Escape is pressed this.element.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isVisible) { this.hide(); } }); // Position the tooltip this.position(); } position() { // Position logic based on this.position and available space // ... } show() { this.tooltip.style.opacity = '1'; this.tooltip.style.visibility = 'visible'; this.isVisible = true; } hide() { this.tooltip.style.opacity = '0'; this.tooltip.style.visibility = 'hidden'; this.isVisible = false; } toggle() { if (this.isVisible) { this.hide(); } else { this.show(); } } } // Initialize all tooltips document.querySelectorAll('[aria-describedby]').forEach(element => { new Tooltip(element); }); </script>

Accessibility Considerations

7. Form Validation and Feedback

Interactive validation provides immediate feedback about form input errors, helping users correct issues before submission.

<!-- HTML Structure --> <form id="signup-form" novalidate> <div class="form-group"> <label for="username">Username:</label> <input type="text" id="username" name="username" required minlength="4" maxlength="20" pattern="[a-zA-Z0-9_]+" aria-describedby="username-hint" > <div id="username-hint" class="form-hint"> 4-20 characters, letters, numbers, and underscores only </div> <div id="username-error" class="form-error" aria-live="polite"></div> </div> <div class="form-group"> <label for="email">Email:</label> <input type="email" id="email" name="email" required aria-describedby="email-error" > <div id="email-error" class="form-error" aria-live="polite"></div> </div> <div class="form-group"> <label for="password">Password:</label> <input type="password" id="password" name="password" required minlength="8" aria-describedby="password-hint password-error" > <div id="password-hint" class="form-hint"> At least 8 characters with a number and a special character </div> <div id="password-error" class="form-error" aria-live="polite"></div> </div> <button type="submit">Sign Up</button> </form> <!-- CSS --> <style> .form-group { margin-bottom: 20px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } input:focus { outline: none; border-color: #3498db; box-shadow: 0 0 5px rgba(52, 152, 219, 0.5); } /* Validation states */ input.valid { border-color: #2ecc71; } input.invalid { border-color: #e74c3c; } .form-hint { font-size: 14px; color: #7f8c8d; margin-top: 5px; } .form-error { font-size: 14px; color: #e74c3c; margin-top: 5px; min-height: 20px; /* Prevent layout shift */ } button { padding: 10px 15px; background-color: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; } button:hover { background-color: #2980b9; } </style> <!-- JavaScript --> <script> const form = document.getElementById('signup-form'); const inputs = form.querySelectorAll('input'); // Custom validation messages const validationMessages = { username: { valueMissing: 'Username is required', tooShort: 'Username must be at least 4 characters', tooLong: 'Username cannot be longer than 20 characters', patternMismatch: 'Username can only contain letters, numbers, and underscores' }, email: { valueMissing: 'Email is required', typeMismatch: 'Please enter a valid email address' }, password: { valueMissing: 'Password is required', tooShort: 'Password must be at least 8 characters', patternMismatch: 'Password must include at least one number and one special character' } }; // Validate single input function validateInput(input) { const field = input.name; const errorElement = document.getElementById(`${field}-error`); // Clear previous error errorElement.textContent = ''; input.classList.remove('valid', 'invalid'); input.setAttribute('aria-invalid', 'false'); // Check validity if (!input.validity.valid) { input.classList.add('invalid'); input.setAttribute('aria-invalid', 'true'); // Get appropriate error message let message = ''; if (input.validity.valueMissing) { message = validationMessages[field].valueMissing; } else if (input.validity.tooShort) { message = validationMessages[field].tooShort; } else if (input.validity.tooLong) { message = validationMessages[field].tooLong; } else if (input.validity.typeMismatch) { message = validationMessages[field].typeMismatch; } else if (input.validity.patternMismatch) { message = validationMessages[field].patternMismatch; } errorElement.textContent = message; return false; } else { input.classList.add('valid'); return true; } } // Add validation listeners to all inputs inputs.forEach(input => { // Validate on blur input.addEventListener('blur', () => { validateInput(input); }); // Real-time validation for better feedback input.addEventListener('input', () => { if (input.classList.contains('invalid')) { validateInput(input); } }); }); // Form submission form.addEventListener('submit', (e) => { let isValid = true; // Validate all inputs inputs.forEach(input => { if (!validateInput(input)) { isValid = false; } }); // Prevent submission if invalid if (!isValid) { e.preventDefault(); // Focus the first invalid input const firstInvalid = form.querySelector('input.invalid'); if (firstInvalid) { firstInvalid.focus(); } // Announce validation errors const formError = document.createElement('div'); formError.setAttribute('role', 'alert'); formError.textContent = 'There are errors in the form. Please correct them and try again.'; form.prepend(formError); // Remove the alert after a delay setTimeout(() => { formError.remove(); }, 5000); } }); </script>

Accessibility Considerations

8. Drag and Drop Interfaces

Drag and drop interfaces allow users to directly manipulate elements on the page, often for reordering or organization.

<!-- HTML Structure --> <div class="drag-container"> <h2 id="drag-heading">Task List</h2> <p id="drag-instructions" class="sr-only"> Use the grab handle to drag items and reorder the list. Press Space to pick up, use arrow keys to reorder, and Space to drop. </p> <ul id="sortable-list" class="sortable-list" aria-labelledby="drag-heading" aria-describedby="drag-instructions" > <li class="sortable-item" draggable="true" aria-roledescription="sortable item" aria-grabbed="false" > <div class="drag-handle" tabindex="0" aria-label="Drag handle">☰</div> <div class="item-content">Task 1: Complete project proposal</div> </li> <li class="sortable-item" draggable="true" aria-roledescription="sortable item" aria-grabbed="false" > <div class="drag-handle" tabindex="0" aria-label="Drag handle">☰</div> <div class="item-content">Task 2: Review client feedback</div> </li> <li class="sortable-item" draggable="true" aria-roledescription="sortable item" aria-grabbed="false" > <div class="drag-handle" tabindex="0" aria-label="Drag handle">☰</div> <div class="item-content">Task 3: Update documentation</div> </li> <li class="sortable-item" draggable="true" aria-roledescription="sortable item" aria-grabbed="false" > <div class="drag-handle" tabindex="0" aria-label="Drag handle">☰</div> <div class="item-content">Task 4: Schedule team meeting</div> </li> </ul> </div> <!-- CSS --> <style> .sortable-list { list-style: none; padding: 0; margin: 0; } .sortable-item { display: flex; align-items: center; padding: 12px; margin-bottom: 8px; background-color: white; border: 1px solid #ddd; border-radius: 4px; transition: background-color 0.2s; } .sortable-item.dragging { opacity: 0.5; background-color: #f5f5f5; } .sortable-item.drag-over { border-top: 2px solid #3498db; } .drag-handle { cursor: grab; padding: 5px 10px; margin-right: 10px; color: #999; } .drag-handle:focus { outline: 2px solid #3498db; outline-offset: 2px; } .drag-handle:active { cursor: grabbing; } .item-content { flex: 1; } .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; } </style> <!-- JavaScript --> <script> const sortableList = document.getElementById('sortable-list'); const items = sortableList.querySelectorAll('.sortable-item'); const dragHandles = sortableList.querySelectorAll('.drag-handle'); let draggedItem = null; // Announce changes to screen readers function announceChange(message) { let liveRegion = document.getElementById('drag-live'); if (!liveRegion) { liveRegion = document.createElement('div'); liveRegion.id = 'drag-live'; liveRegion.setAttribute('aria-live', 'assertive'); liveRegion.className = 'sr-only'; document.body.appendChild(liveRegion); } liveRegion.textContent = message; } // Initialize items.forEach(item => { // Mouse drag events item.addEventListener('dragstart', function() { draggedItem = this; setTimeout(() => { this.classList.add('dragging'); this.setAttribute('aria-grabbed', 'true'); }, 0); // Announce to screen readers const itemText = this.querySelector('.item-content').textContent; announceChange(`Grabbed ${itemText}`); }); item.addEventListener('dragend', function() { this.classList.remove('dragging'); this.setAttribute('aria-grabbed', 'false'); // Announce to screen readers const itemText = this.querySelector('.item-content').textContent; const newPosition = Array.from(sortableList.children).indexOf(this) + 1; announceChange(`${itemText} moved to position ${newPosition} of ${items.length}`); }); // Handle draghandles for keyboard accessibility const dragHandle = item.querySelector('.drag-handle'); // Space to grab/drop dragHandle.addEventListener('keydown', function(e) { if (e.code === 'Space' || e.key === ' ') { e.preventDefault(); if (draggedItem === item) { // Drop draggedItem.classList.remove('dragging'); draggedItem.setAttribute('aria-grabbed', 'false'); draggedItem = null; } else { // Pick up if (draggedItem) { // Drop previous item if any draggedItem.classList.remove('dragging'); draggedItem.setAttribute('aria-grabbed', 'false'); } // Pick up new item draggedItem = item; draggedItem.classList.add('dragging'); draggedItem.setAttribute('aria-grabbed', 'true'); // Announce to screen readers const itemText = draggedItem.querySelector('.item-content').textContent; announceChange(`Grabbed ${itemText}. Use arrow keys to reorder.`); } } // Arrow keys to move if (draggedItem === item) { const currentIndex = Array.from(sortableList.children).indexOf(draggedItem); if (e.key === 'ArrowUp' && currentIndex > 0) { e.preventDefault(); const previousItem = sortableList.children[currentIndex - 1]; sortableList.insertBefore(draggedItem, previousItem); // Focus moved item's drag handle draggedItem.querySelector('.drag-handle').focus(); announceChange(`Moved up to position ${currentIndex}`); } if (e.key === 'ArrowDown' && currentIndex < sortableList.children.length - 1) { e.preventDefault(); const nextItem = sortableList.children[currentIndex + 2]; // +2 because the current item is still in the list if (nextItem) { sortableList.insertBefore(draggedItem, nextItem); } else { sortableList.appendChild(draggedItem); } // Focus moved item's drag handle draggedItem.querySelector('.drag-handle').focus(); announceChange(`Moved down to position ${currentIndex + 2}`); } } }); }); // Handle drop targets sortableList.addEventListener('dragover', function(e) { e.preventDefault(); const afterElement = getDragAfterElement(sortableList, e.clientY); const draggable = document.querySelector('.dragging'); if (afterElement == null) { sortableList.appendChild(draggable); } else { sortableList.insertBefore(draggable, afterElement); } }); // Helper function to determine drag position function getDragAfterElement(container, y) { const draggableElements = [...container.querySelectorAll('.sortable-item:not(.dragging)')]; return draggableElements.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; if (offset < 0 && offset > closest.offset) { return { offset: offset, element: child }; } else { return closest; } }, { offset: Number.NEGATIVE_INFINITY }).element; } </script>

Accessibility Considerations

9. Infinite Scroll

Infinite scroll automatically loads more content as the user scrolls down, providing a seamless browsing experience without pagination.

<!-- HTML Structure --> <div class="infinite-scroll-container"> <h2 id="content-heading">Article List</h2> <div role="feed" aria-busy="false" aria-labelledby="content-heading" > <ul id="content-list" class="content-list"> <!-- Initial items --> <li class="content-item"> <article> <h3>Article Title 1<