JavaScript Accessibility: 8 ARIA Patterns You Need

Learn the essential ARIA patterns every JavaScript developer needs to build accessible web applications. From modal dialogs to custom dropdowns, master focus management and screen reader support.
While I was looking over some legacy code the other day, I stumbled upon a custom dropdown component that made me cringe. Not because the JavaScript was terrible—actually, it was quite elegant—but because it was completely unusable with a keyboard. No ARIA labels, no focus management, nothing. I was once guilty of this too, building beautiful interfaces that left entire groups of users behind.
With the European Accessibility Act now enforced and ADA compliance deadlines hitting in April 2026, accessibility isn't optional anymore. Little did I know back then that learning ARIA patterns would become one of the most valuable skills in my developer toolkit.
Why ARIA Patterns Matter in 2026
Here's the reality: About 15% of the world's population experiences some form of disability. That's over a billion people who might struggle with your carefully crafted interface if you're not thinking about accessibility. I cannot stress this enough! Building accessible applications isn't just about legal compliance—it's about reaching your entire audience.
The wonderful thing about ARIA (Accessible Rich Internet Applications) is that it bridges the gap between modern interactive web components and assistive technologies like screen readers. When I finally decided to dive deep into ARIA, I realized it wasn't as complicated as I thought. You just need to understand a few core patterns.
The Three Rules of ARIA (And When to Break Them)
Before we get into specific patterns, let's talk about the three golden rules of ARIA usage:
Rule 1: Don't use ARIA if you can use semantic HTML instead. A <button> is always better than <div role="button">. Native elements come with built-in keyboard support and semantics.
Rule 2: Don't change native semantics unless absolutely necessary. Don't put role="heading" on a button element. That's just confusing for everyone.
Rule 3: All interactive ARIA controls must be keyboard accessible. If you can click it with a mouse, you should be able to use it with a keyboard.
In other words, use ARIA to enhance what HTML can't do natively—like complex widgets and state management. Now let's look at the patterns you'll actually use in production code.

Pattern 1: Accessible Modal Dialogs with Focus Trapping
Modal dialogs are everywhere, and they're one of the most commonly broken accessibility patterns I've seen. The key challenges are focus management and keyboard navigation. When a modal opens, focus should move inside it and stay trapped until the user closes it.
Here's how I implement accessible modals:
class AccessibleModal {
constructor(modalElement) {
this.modal = modalElement;
this.trigger = null;
this.focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
}
open(triggerElement) {
this.trigger = triggerElement;
this.modal.setAttribute('aria-hidden', 'false');
this.modal.setAttribute('role', 'dialog');
this.modal.setAttribute('aria-modal', 'true');
// Move focus to first focusable element or the modal itself
const firstFocusable = this.modal.querySelector(this.focusableElements);
if (firstFocusable) {
firstFocusable.focus();
} else {
this.modal.setAttribute('tabindex', '-1');
this.modal.focus();
}
// Trap focus inside modal
this.modal.addEventListener('keydown', this.handleKeyDown.bind(this));
document.body.style.overflow = 'hidden';
}
close() {
this.modal.setAttribute('aria-hidden', 'true');
this.modal.removeEventListener('keydown', this.handleKeyDown.bind(this));
document.body.style.overflow = '';
// Return focus to trigger element
if (this.trigger) {
this.trigger.focus();
}
}
handleKeyDown(e) {
const focusableContent = this.modal.querySelectorAll(this.focusableElements);
const firstFocusable = focusableContent[0];
const lastFocusable = focusableContent[focusableContent.length - 1];
// Close on Escape
if (e.key === 'Escape') {
this.close();
return;
}
// Trap Tab key
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
lastFocusable.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastFocusable) {
firstFocusable.focus();
e.preventDefault();
}
}
}
}
}
// Usage
const modal = new AccessibleModal(document.getElementById('myModal'));
document.getElementById('openButton').addEventListener('click', (e) => {
modal.open(e.currentTarget);
});The magic happens in focus trapping. When users press Tab at the last focusable element, we loop back to the first one. Shift+Tab does the reverse. And pressing Escape always closes the modal and returns focus to whatever triggered it.
Pattern 2: Accordion Components with Proper State Management
Accordions are fascinating because they look simple but require careful ARIA orchestration. I came across so many broken accordions in my early days—ones that announced nothing to screen readers or couldn't be navigated with keyboards.
interface AccordionSection {
button: HTMLElement;
panel: HTMLElement;
isExpanded: boolean;
}
class AccessibleAccordion {
private sections: AccordionSection[] = [];
constructor(accordionElement: HTMLElement) {
const buttons = accordionElement.querySelectorAll('[data-accordion-trigger]');
buttons.forEach((button, index) => {
const panel = button.nextElementSibling as HTMLElement;
const buttonId = `accordion-button-${index}`;
const panelId = `accordion-panel-${index}`;
// Set up ARIA attributes
button.setAttribute('id', buttonId);
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-controls', panelId);
panel.setAttribute('id', panelId);
panel.setAttribute('role', 'region');
panel.setAttribute('aria-labelledby', buttonId);
panel.hidden = true;
this.sections.push({
button: button as HTMLElement,
panel,
isExpanded: false
});
button.addEventListener('click', () => this.toggle(index));
button.addEventListener('keydown', (e) => this.handleKeyboard(e, index));
});
}
toggle(index: number) {
const section = this.sections[index];
section.isExpanded = !section.isExpanded;
section.button.setAttribute('aria-expanded', String(section.isExpanded));
section.panel.hidden = !section.isExpanded;
// Optional: Collapse others for single-expand behavior
// this.sections.forEach((s, i) => {
// if (i !== index && s.isExpanded) {
// this.toggle(i);
// }
// });
}
handleKeyboard(event: KeyboardEvent, currentIndex: number) {
const { key } = event;
let newIndex = currentIndex;
switch (key) {
case 'ArrowDown':
newIndex = (currentIndex + 1) % this.sections.length;
event.preventDefault();
break;
case 'ArrowUp':
newIndex = currentIndex === 0 ? this.sections.length - 1 : currentIndex - 1;
event.preventDefault();
break;
case 'Home':
newIndex = 0;
event.preventDefault();
break;
case 'End':
newIndex = this.sections.length - 1;
event.preventDefault();
break;
default:
return;
}
this.sections[newIndex].button.focus();
}
}
// Initialize
new AccessibleAccordion(document.querySelector('[data-accordion]'));The key insight here is the relationship between aria-controls and aria-labelledby. Screen readers use these to understand that the button controls the panel, and the panel's label comes from the button. Luckily we can also add keyboard navigation with arrow keys, which is something users expect from accordions.
Pattern 3: Custom Select Dropdowns with Keyboard Navigation
Native <select> elements are accessible by default, but sometimes you need custom styling or behavior. When I realized how much work goes into making a truly accessible custom select, I gained a new appreciation for browser defaults.
The pattern requires managing focus, keyboard events, and ARIA states. You need role="combobox" on the trigger, role="listbox" on the dropdown, and role="option" on each item. Plus you have to handle aria-expanded, aria-activedescendant, and keyboard navigation for Up/Down/Enter/Escape keys.

Pattern 4: Tab Panels with ARIA Live Regions
Tab interfaces need role="tablist" on the container, role="tab" on each tab button, and role="tabpanel" on each content area. The critical piece is aria-selected on the active tab and making sure only one tab is in the focus order at a time using tabindex="-1" on inactive tabs.
When users navigate with arrow keys, focus should move between tabs. When they press Enter or Space, the content should update. If you're dynamically loading content, consider adding aria-live="polite" to announce changes to screen reader users.
Pattern 5: Tooltips and Popovers with aria-describedby
I cannot stress this enough! Tooltips are not the same as title attributes. A proper accessible tooltip requires role="tooltip" and should be referenced using aria-describedby on the element it describes.
For popovers that contain interactive content, use role="dialog" or role="region" instead, and manage focus appropriately. The pattern is similar to modals but typically less intrusive—users can often tab out of popovers without being trapped.
Patterns 6-8: Carousels, Date Pickers, and Alerts
Carousels need clear labels (aria-label="Featured Products"), roving tabindex for the items, and pause/play controls. Auto-rotating carousels should pause on hover and focus.
Date pickers are complex beasts requiring a grid pattern with role="grid", proper week/day labels, and keyboard navigation that feels intuitive. In other words, arrow keys should move between days, Page Up/Down should change months, and Home/End should jump to week boundaries.
Alert regions use role="alert" for important messages that need immediate attention or aria-live="polite" for less urgent updates. I've learned to be conservative with alerts—too many and users tune them out.
Testing Your ARIA Implementation
Here's my testing workflow:
-
Keyboard only: Unplug your mouse and navigate your entire application. Can you reach everything? Is the focus visible?
-
Screen reader testing: Use NVDA (Windows) or VoiceOver (Mac). Does everything make sense when you can't see it?
-
Automated tools: Run axe DevTools or Lighthouse. They catch about 30-40% of issues automatically.
-
Manual code review: Check that ARIA attributes make semantic sense and follow the patterns.
The wonderful thing about this approach is that you catch different types of issues at each level. Automated tools find obvious problems, keyboard testing reveals focus issues, and screen readers expose confusing semantics.
Building Accessible Interfaces That Last
When I started focusing on accessibility, I thought it would slow me down. Little did I know that it would actually make me a better developer. Thinking about keyboard navigation, focus management, and semantic structure improved all my code—not just the accessible parts.
The patterns we've covered here aren't exhaustive, but they're the ones I use most frequently in production applications. Modal dialogs, accordions, and custom selects show up in almost every project. Master these, and you're well on your way to building interfaces that work for everyone.
Remember: ARIA is a enhancement tool, not a replacement for good HTML. Start with semantic elements, add ARIA when needed, and always test with actual assistive technologies. Your users will thank you.
And that concludes the end of this post! I hope you found this valuable and look out for more in the future!