Warning: Don't use <div> or <span> for buttons unless
absolutely necessary. Native <button> elements work better with assistive tech and handle
keyboard interaction automatically.
disabled vs aria-disabled
Behavior
When to Use
disabled attribute
Not focusable, not in tab order, grayed out
Standard disabled state - user can't interact
aria-disabled="true"
Still focusable, in tab order, can show tooltip
Need to explain why disabled (tooltip on focus)
2. Modal and Dialog Implementation (role="dialog", aria-modal)
Warning: Combobox is one of the most complex ARIA patterns. Use native
<select> or <datalist> when possible. Only implement custom combobox when
absolutely necessary.
Note: There are two tab activation patterns: Automatic (arrow
keys activate immediately - recommended) and Manual (arrow keys focus,
Enter/Space activates). Automatic is more common and preferred.
5. Accordion and Disclosure Widgets (aria-expanded)
Pattern
HTML Element
ARIA Pattern
Use Case
Native Disclosure
<details> + <summary>
None (built-in)
Simple show/hide content (preferred)
ARIA Disclosure
<button aria-expanded>
aria-expanded, aria-controls
Custom styled disclosure
Accordion
Multiple disclosure widgets
Same as disclosure, grouped
FAQ sections, settings panels
Example: Native details/summary (preferred)
<!-- Native disclosure (best accessibility) --><details> <summary>What is accessibility?</summary> <p> Accessibility ensures that websites and applications can be used by everyone, including people with disabilities who may use assistive technologies like screen readers. </p></details><details open> <summary>Why is it important?</summary> <p> It's not just a legal requirement - it makes your content available to a wider audience and improves usability for everyone. </p></details><style>details { border: 1px solid #ddd; border-radius: 4px; padding: 12px; margin-bottom: 8px;}summary { cursor: pointer; font-weight: 600; padding: 8px 0; list-style: none; /* Remove default marker */ display: flex; align-items: center;}/* Custom marker */summary::before { content: "▶"; margin-right: 8px; transition: transform 0.2s; display: inline-block;}details[open] summary::before { transform: rotate(90deg);}/* Remove default marker in WebKit */summary::-webkit-details-marker { display: none;}summary:focus { outline: 2px solid #0066cc; outline-offset: 2px; border-radius: 2px;}details p { margin-top: 12px; padding-left: 24px;}</style>
Note: Use native <details>/<summary> when possible - it requires no
JavaScript, works in all modern browsers, and has excellent accessibility support.
6. Custom Widget Development
Development Step
Consideration
Resources
1. Check Native Options
Can native HTML element meet needs?
HTML5 elements, form controls
2. Review ARIA Patterns
Is there an established ARIA pattern?
WAI-ARIA Authoring Practices Guide (APG)
3. Define Keyboard Interaction
What keys should do what?
Follow APG keyboard patterns
4. Implement ARIA
Roles, states, properties
ARIA specification
5. Add Focus Management
Where should focus go when?
Focus trap, roving tabindex patterns
6. Test with AT
Screen readers, keyboard only
NVDA, JAWS, VoiceOver, TalkBack
Example: Custom widget checklist and template
// Custom Widget Development Templateclass CustomWidget { constructor(element) { this.element = element; this.isInitialized = false; // Validate element exists if (!this.element) { console.error('Widget element not found'); return; } this.init(); } init() { // 1. Set ARIA roles and initial state this.setupARIA(); // 2. Setup keyboard interaction this.setupKeyboard(); // 3. Setup mouse/touch interaction this.setupPointer(); // 4. Setup focus management this.setupFocus(); this.isInitialized = true; } setupARIA() { // Set role if not already present if (!this.element.hasAttribute('role')) { this.element.setAttribute('role', 'widget-role'); } // Set initial ARIA states this.element.setAttribute('aria-label', 'Widget name'); this.element.setAttribute('tabindex', '0'); // Set dynamic ARIA properties this.updateARIAState(); } updateARIAState() { // Update ARIA states based on widget state // Example: aria-expanded, aria-selected, aria-checked, etc. } setupKeyboard() { this.element.addEventListener('keydown', (e) => { switch(e.key) { case 'Enter': case ' ': e.preventDefault(); this.activate(); break; case 'ArrowUp': case 'ArrowDown': case 'ArrowLeft': case 'ArrowRight': e.preventDefault(); this.navigate(e.key); break; case 'Home': e.preventDefault(); this.navigateToFirst(); break; case 'End': e.preventDefault(); this.navigateToLast(); break; case 'Escape': e.preventDefault(); this.close(); break; case 'Tab': // Allow default tab behavior unless in focus trap if (this.isFocusTrapped) { e.preventDefault(); this.handleTabInTrap(e.shiftKey); } break; } }); } setupPointer() { // Click events this.element.addEventListener('click', (e) => { this.handleClick(e); }); // Touch events for mobile this.element.addEventListener('touchstart', (e) => { this.handleTouch(e); }, { passive: true }); } setupFocus() { // Focus events this.element.addEventListener('focus', () => { this.onFocus(); }); this.element.addEventListener('blur', () => { this.onBlur(); }); } // Widget-specific methods activate() { console.log('Widget activated'); this.updateARIAState(); } navigate(direction) { console.log('Navigate:', direction); } navigateToFirst() { console.log('Navigate to first'); } navigateToLast() { console.log('Navigate to last'); } close() { console.log('Widget closed'); } handleClick(e) { console.log('Clicked:', e.target); } handleTouch(e) { console.log('Touch:', e.target); } onFocus() { this.element.classList.add('focused'); } onBlur() { this.element.classList.remove('focused'); } handleTabInTrap(isShiftTab) { // Implement focus trap logic } // Public API destroy() { // Clean up event listeners this.element.removeAttribute('role'); this.element.removeAttribute('tabindex'); this.isInitialized = false; }}// Usageconst widget = new CustomWidget(document.getElementById('my-widget'));
Widget Checklist
✓ Verified
Test Method
Keyboard Accessible
☐
Unplug mouse, navigate with keyboard only
Screen Reader Compatible
☐
Test with NVDA/JAWS/VoiceOver
Proper ARIA Roles
☐
Verify with browser DevTools
States Update Dynamically
☐
Check ARIA attributes change with state
Focus Visible
☐
Clear focus indicator on all elements
Focus Management
☐
Focus moves logically through widget
Touch Targets ≥44px
☐
Test on mobile device
Works Without JS
☐
Progressive enhancement (if possible)
Respects User Preferences
☐
Test prefers-reduced-motion, color schemes
Error States Announced
☐
Use aria-live or role="alert"
Cross-Browser Tested
☐
Chrome, Firefox, Safari, Edge
Mobile AT Tested
☐
iOS VoiceOver, Android TalkBack
Common Widget Types
ARIA Pattern Reference
Complexity
Accordion
button[aria-expanded] + region
Low
Tabs
tablist + tab + tabpanel
Medium
Dialog/Modal
dialog + focus trap
Medium
Dropdown Menu
menu + menuitem
Medium
Combobox
combobox + listbox + active descendant
High
Slider
slider + aria-valuemin/max/now
Medium
Data Grid
grid + row + gridcell
High
Tree View
tree + treeitem + nested structure
High
Tooltip
tooltip + aria-describedby
Low
Carousel
region + group + rotation controls
Medium-High
Warning: Custom widgets are difficult to get right. Before building custom, ask: Can native HTML solve this? Then: Can existing library solve
this? Only build custom when absolutely necessary.
Resources:
ARIA Authoring Practices Guide (APG): w3.org/WAI/ARIA/apg/ - Definitive patterns for all
widgets