Accessible Component Patterns: Modals, Tables, Carousels, and Beyond
Modern web applications are built from reusable components — modals, data tables, tabs, carousels, dropdown menus, and more. When these components are built accessibly from the start, accessibility scales with the application. When they are not, inaccessible patterns replicate across every page where the component appears.
This guide covers the most common interactive component patterns and the specific accessibility requirements each one demands. Every pattern here is grounded in the WAI-ARIA Authoring Practices, the definitive reference for building accessible widgets. The goal is not just WCAG compliance but genuine usability for people using keyboards, screen readers, voice control, and other assistive technologies.
Why Component Accessibility Matters
A component-based architecture means a single accessibility fix — or a single accessibility failure — propagates everywhere that component is used. If your modal component traps focus correctly, every modal in your application works. If it does not, every modal is broken.
This leverage makes components the highest-impact place to invest in accessibility. Getting the patterns right at the component level delivers several advantages:
- Scalable compliance — fix a pattern once and it applies across every instance in the codebase
- Consistent user experience — assistive technology users develop expectations about how components behave; consistency reduces cognitive load
- Reduced testing burden — a well-tested component library means individual pages need less manual accessibility testing
- Developer education — accessible component patterns serve as working documentation for the entire team
Modal Dialogs
Modal dialogs are among the most complex accessible components. They interrupt the user's workflow, take over focus, and must be fully operable without a mouse. Getting modals wrong creates severe barriers — users can become trapped, unable to close the dialog, or they may not even know a dialog has appeared.
Focus management is the most critical requirement. When a modal opens, focus must move into the dialog. Focus must then be trapped within the dialog — Tab and Shift+Tab should cycle through focusable elements inside the modal without ever escaping to the page behind it. When the modal closes, focus must return to the element that triggered it.
Keyboard interaction:
Escapecloses the dialogTabmoves forward through focusable elements inside the dialog, wrapping from last to firstShift+Tabmoves backward, wrapping from first to last
Required ARIA attributes:
role="dialog"on the modal container (orrole="alertdialog"for urgent confirmations)aria-modal="true"to indicate that content behind the dialog is inertaria-labelledbypointing to the dialog's heading, providing an accessible namearia-describedbyoptionally pointing to descriptive content within the dialog
The underlying page content should be made inert when the modal is open. The HTML inert attribute on the main page content is the modern approach. A visible backdrop overlay reinforces visually that the background is inactive.
<!-- Trigger button -->
<button id="open-modal" aria-haspopup="dialog">Delete account</button>
<!-- Modal dialog -->
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
<h2 id="modal-title">Confirm account deletion</h2>
<p>This action cannot be undone. All your data will be permanently removed.</p>
<button>Cancel</button>
<button>Delete permanently</button>
</div>
Data Tables
Data tables are essential for presenting structured information, but they are frequently inaccessible. Screen readers rely on proper table markup to announce row and column relationships. Without it, a table becomes an incomprehensible stream of disconnected values.
Basic table structure:
- Use
<table>,<thead>,<tbody>, and<tfoot>for proper structure - Provide a
<caption>element to give the table an accessible name that describes its purpose - Use
<th>elements for header cells, never styled<td>elements - Add
scope="col"to column headers andscope="row"to row headers
<table>
<caption>Quarterly revenue by region (in thousands)</caption>
<thead>
<tr>
<th scope="col">Region</th>
<th scope="col">Q1</th>
<th scope="col">Q2</th>
<th scope="col">Q3</th>
<th scope="col">Q4</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Europe</th>
<td>1,200</td>
<td>1,350</td>
<td>1,100</td>
<td>1,500</td>
</tr>
</tbody>
</table>
Complex tables with multi-level headers or merged cells require a more explicit approach. Use the headers attribute on data cells combined with id attributes on header cells to create explicit associations:
<table>
<caption>Course schedule by department and semester</caption>
<thead>
<tr>
<td></td>
<th id="fall" colspan="2" scope="colgroup">Fall</th>
<th id="spring" colspan="2" scope="colgroup">Spring</th>
</tr>
<tr>
<td></td>
<th id="fall-lec" scope="col">Lectures</th>
<th id="fall-lab" scope="col">Labs</th>
<th id="spring-lec" scope="col">Lectures</th>
<th id="spring-lab" scope="col">Labs</th>
</tr>
</thead>
<tbody>
<tr>
<th id="cs" scope="row">Computer Science</th>
<td headers="cs fall fall-lec">12</td>
<td headers="cs fall fall-lab">8</td>
<td headers="cs spring spring-lec">14</td>
<td headers="cs spring spring-lab">10</td>
</tr>
</tbody>
</table>
Responsive tables: Avoid layouts that break the table structure on small screens. Common approaches include horizontal scrolling with an outer container (overflow-x: auto with role="region" and aria-label on the wrapper), or restructuring data into a stacked card layout on narrow viewports. Never use CSS to rearrange table cells in a way that disconnects headers from data.
Carousels and Sliders
Carousels present unique accessibility challenges because they combine dynamic content, automatic advancement, and compact navigation controls. Many carousels are entirely inaccessible — keyboard users cannot reach the content, auto-rotation distracts and disorients, and screen reader users have no awareness of the changing content.
Essential requirements:
- Pause and stop controls — any auto-advancing carousel must have a visible, keyboard-accessible button to pause or stop rotation (WCAG 2.2.2 Pause, Stop, Hide)
- Keyboard navigation — users must be able to move between slides using Previous/Next buttons or direct slide indicators, all operable with the keyboard
- Screen reader announcements — use
aria-live="polite"on the slide container so that screen readers announce new content when slides change, but without interrupting the user's current action - Slide indicators — provide controls that indicate the current slide and allow direct navigation (e.g., dot indicators with
aria-label="Slide 3 of 5"andaria-current="true"on the active slide)
<div role="region" aria-roledescription="carousel" aria-label="Featured articles">
<button aria-label="Pause auto-rotation">Pause</button>
<div aria-live="polite">
<div role="group" aria-roledescription="slide" aria-label="1 of 4">
<!-- Slide content -->
</div>
</div>
<button aria-label="Previous slide">«</button>
<button aria-label="Next slide">»</button>
<div role="tablist" aria-label="Slide controls">
<button role="tab" aria-selected="true" aria-label="Slide 1"></button>
<button role="tab" aria-selected="false" aria-label="Slide 2"></button>
<button role="tab" aria-selected="false" aria-label="Slide 3"></button>
<button role="tab" aria-selected="false" aria-label="Slide 4"></button>
</div>
</div>
When auto-rotation is active, set aria-live="off" to prevent constant screen reader interruptions, and switch to aria-live="polite" when the user pauses rotation or manually navigates.
Accordions
Accordions allow users to expand and collapse sections of content, reducing visual clutter while keeping information accessible. They are widely used for FAQs, settings panels, and content-heavy pages.
Keyboard interaction:
EnterorSpaceon a focused accordion header toggles the associated panel between expanded and collapsedArrow Downmoves focus to the next accordion headerArrow Upmoves focus to the previous accordion headerHomemoves focus to the first accordion headerEndmoves focus to the last accordion header
Required ARIA attributes:
- Each accordion header should be a
<button>(or haverole="button") wrapped in an appropriate heading level aria-expanded="true"or"false"on the button to indicate the current state of the panelaria-controlson the button, pointing to theidof the associated panel- The panel region can use
role="region"witharia-labelledbypointing back to the header
<div class="accordion">
<h3>
<button aria-expanded="true" aria-controls="panel-1" id="header-1">
Shipping information
</button>
</h3>
<div id="panel-1" role="region" aria-labelledby="header-1">
<p>We ship to over 50 countries. Standard delivery takes 5-7 business days.</p>
</div>
<h3>
<button aria-expanded="false" aria-controls="panel-2" id="header-2">
Return policy
</button>
</h3>
<div id="panel-2" role="region" aria-labelledby="header-2" hidden>
<p>Items can be returned within 30 days of purchase for a full refund.</p>
</div>
</div>
The native HTML <details> and <summary> elements provide built-in accordion behavior with correct semantics and keyboard support. For simple use cases, they are preferable to custom implementations.
Tabs
Tabs organize content into multiple panels, displaying one panel at a time. They are one of the most precisely specified patterns in the ARIA Authoring Practices, and getting the roles and keyboard behavior right is essential.
Required roles:
role="tablist"on the container that holds all the tab elementsrole="tab"on each tab trigger elementrole="tabpanel"on each content panel associated with a tab
Required ARIA attributes:
aria-selected="true"on the active tab,"false"on all othersaria-controlson each tab, pointing to its associated tabpanel'sidaria-labelledbyon each tabpanel, pointing to its associated tab'sid- Only the active tab should be in the tab order (
tabindex="0"); inactive tabs should havetabindex="-1"
Keyboard interaction:
Arrow Left/Arrow Rightmoves focus between tabs (horizontal tab list)Arrow Up/Arrow Downfor vertical tab listsHomemoves focus to the first tabEndmoves focus to the last tabTabmoves focus from the active tab into the associated tabpanel
Activation model: There are two valid approaches. With automatic activation, a tab is selected as soon as it receives focus via arrow keys. With manual activation, arrow keys move focus but the user must press Enter or Space to activate the tab. Automatic activation is recommended when tab panels load instantly. Manual activation is preferable when panel content is loaded asynchronously or the switch has side effects.
<div role="tablist" aria-label="Account settings">
<button role="tab" id="tab-profile" aria-selected="true"
aria-controls="panel-profile" tabindex="0">Profile</button>
<button role="tab" id="tab-security" aria-selected="false"
aria-controls="panel-security" tabindex="-1">Security</button>
<button role="tab" id="tab-billing" aria-selected="false"
aria-controls="panel-billing" tabindex="-1">Billing</button>
</div>
<div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile" tabindex="0">
<!-- Profile settings content -->
</div>
<div role="tabpanel" id="panel-security" aria-labelledby="tab-security"
tabindex="0" hidden>
<!-- Security settings content -->
</div>
<div role="tabpanel" id="panel-billing" aria-labelledby="tab-billing"
tabindex="0" hidden>
<!-- Billing settings content -->
</div>
Dropdown Menus
Dropdown menus — also called menu buttons — combine a trigger button with a list of actions or options. They are functionally different from navigation menus: a dropdown menu presents a set of commands or choices, while a navigation menu provides links to other pages.
Required ARIA attributes:
- The trigger button uses
aria-haspopup="true"(or"menu") andaria-expandedto indicate the menu's open state - The menu container uses
role="menu" - Each menu item uses
role="menuitem" - For checkable items, use
role="menuitemcheckbox"orrole="menuitemradio"
Keyboard interaction:
Enter,Space, orArrow Downon the trigger button opens the menu and focuses the first itemArrow Upon the trigger opens the menu and focuses the last itemArrow Down/Arrow Upnavigates between menu itemsEnterorSpaceactivates the focused menu itemEscapecloses the menu and returns focus to the trigger button- Typeahead — typing a character moves focus to the next menu item whose label starts with that character
Homemoves focus to the first item,Endto the last
<div class="menu-container">
<button aria-haspopup="menu" aria-expanded="false" aria-controls="actions-menu">
Actions
</button>
<ul role="menu" id="actions-menu" hidden>
<li role="menuitem" tabindex="-1">Edit</li>
<li role="menuitem" tabindex="-1">Duplicate</li>
<li role="separator"></li>
<li role="menuitem" tabindex="-1">Archive</li>
<li role="menuitem" tabindex="-1">Delete</li>
</ul>
</div>
Only one item in the menu should have tabindex="0" (or be the active descendant via aria-activedescendant). All other items should have tabindex="-1". Focus is managed programmatically within the menu using arrow keys.
Tooltips
Tooltips provide supplementary descriptions for interface elements. They appear on hover or focus and offer brief clarifying text. While they seem simple, tooltips have several accessibility requirements that are frequently overlooked.
Key requirements:
- Keyboard accessible — tooltips must be triggered by keyboard focus, not just mouse hover. The trigger element must be focusable (a button, link, or element with
tabindex="0") - Dismissable — the
Escapekey must dismiss the tooltip without moving focus (WCAG 1.4.13 Content on Hover or Focus) - Hoverable — users must be able to move their pointer over the tooltip content without it disappearing
- Persistent — the tooltip should remain visible until the user actively dismisses it, moves focus, or moves the pointer away
- No essential information — tooltips should not be the only way to access critical information, since they are inherently less discoverable
ARIA approach: Use role="tooltip" on the tooltip element, and connect it to the trigger using aria-describedby. This ensures that when a screen reader user focuses the trigger, the tooltip content is announced as a description.
<button aria-describedby="tooltip-save">
<svg aria-hidden="true"><!-- save icon --></svg>
Save
</button>
<div role="tooltip" id="tooltip-save">Save changes to draft (Ctrl+S)</div>
Do not use aria-label for tooltip-style text — aria-label replaces the accessible name, while aria-describedby supplements it. If the tooltip provides the element's name (not supplementary info), use aria-labelledby instead.
Autocomplete and Combobox
Autocomplete inputs — search boxes with suggestions, address fields with predictions, tag selectors — are among the most complex ARIA patterns. The combobox pattern combines a text input with a popup list of options, and requires careful coordination between the input, the list, and screen reader announcements.
Required ARIA attributes:
role="combobox"on the text inputaria-autocompleteset to"list"(suggestions shown),"inline"(autocomplete text appears in the input), or"both"aria-expanded="true"when the suggestion list is visible,"false"when hiddenaria-controlspointing to the suggestion list'sidaria-activedescendanton the input, pointing to theidof the currently highlighted option — this lets screen readers announce the focused option while keeping DOM focus in the text inputrole="listbox"on the suggestion listrole="option"on each suggestion, witharia-selected="true"on the active one
Announcing results: When the list of suggestions updates, announce the number of results to screen reader users. A live region works well for this: an element with aria-live="polite" that receives text such as "8 suggestions available" or "No results found."
<label for="city-search">City</label>
<input id="city-search" type="text"
role="combobox"
aria-autocomplete="list"
aria-expanded="true"
aria-controls="city-listbox"
aria-activedescendant="city-opt-2">
<ul role="listbox" id="city-listbox">
<li role="option" id="city-opt-1">Copenhagen</li>
<li role="option" id="city-opt-2" aria-selected="true">Cologne</li>
<li role="option" id="city-opt-3">Colombo</li>
</ul>
<div aria-live="polite" class="sr-only">3 suggestions available</div>
Keyboard interaction:
Arrow Down/Arrow Upnavigates through suggestionsEnterselects the highlighted suggestionEscapecloses the suggestion list (pressing again clears the input)- Typing updates the list; the input retains DOM focus throughout
Toast Notifications
Toast notifications are brief, non-modal messages that appear to inform users about the outcome of an action — a form submission, a saved change, an error. They are transient by nature, which creates specific accessibility challenges: the notification must be perceivable by screen reader users without disrupting their workflow.
Key requirements:
- Live region: Toast containers should use
aria-live="polite"so screen readers announce new notifications without interrupting the current speech. For urgent errors,aria-live="assertive"orrole="alert"is appropriate - Sufficient display time: Auto-dismissing toasts must remain visible long enough for users to read them. WCAG does not specify an exact duration, but 5-8 seconds is a reasonable minimum. Users with cognitive disabilities or slow reading speeds need more time
- Dismissible: Users must be able to close the notification manually. A visible close button with an accessible label is required
- Non-blocking: Toast notifications should not prevent the user from interacting with the rest of the page
- Not focus-stealing: Toasts should not move focus unless the notification is critical (such as an action that requires immediate user response — in that case, use a modal dialog instead)
<!-- Toast container (always present in the DOM) -->
<div aria-live="polite" aria-atomic="true" class="toast-container">
<!-- Toasts are injected here dynamically -->
<div role="status" class="toast">
<p>Settings saved successfully.</p>
<button aria-label="Dismiss notification">×</button>
</div>
</div>
The aria-atomic="true" attribute ensures the entire toast content is announced as a single unit rather than only the portion that changed. Place the live region container in the DOM on page load and inject toast content into it; adding the live region dynamically at the same time as the content may cause screen readers to miss the announcement.
Date Pickers
Date pickers are notoriously complex accessible components. They must support keyboard navigation through a calendar grid, communicate the selected date, and ideally offer a text input as an alternative for users who prefer to type a date directly.
The grid pattern: The calendar view uses a table with role="grid". Each day cell is a gridcell. Row and column headers communicate the day-of-week and week structure.
Keyboard interaction:
Arrow Rightmoves to the next dayArrow Leftmoves to the previous dayArrow Downmoves to the same day in the next weekArrow Upmoves to the same day in the previous weekHomemoves to the first day of the current weekEndmoves to the last day of the current weekPage Upmoves to the previous monthPage Downmoves to the next monthShift+Page Upmoves to the previous yearShift+Page Downmoves to the next yearEnterorSpaceselects the focused date
Essential ARIA:
aria-labelon the grid describing the current month and year (e.g., "March 2026")aria-selected="true"on the selected date- Previous/Next month buttons with clear accessible labels
- The currently focused date should be the only cell with
tabindex="0"; all other cells should havetabindex="-1"
Always offer a text input alternative. Some users find calendar grids difficult or impossible to use. A text input that accepts common date formats (with clear format guidance via placeholder or help text) ensures that everyone can enter a date. The input and calendar should remain synchronized.
<label for="departure-date">Departure date</label>
<input id="departure-date" type="text" placeholder="DD/MM/YYYY"
aria-describedby="date-format-hint">
<span id="date-format-hint" class="sr-only">Format: day, month, year</span>
<button aria-label="Choose date from calendar">
<svg aria-hidden="true"><!-- calendar icon --></svg>
</button>
<!-- Calendar dialog opens on button click -->
<div role="dialog" aria-modal="true" aria-label="Choose departure date">
<div class="calendar-header">
<button aria-label="Previous month">«</button>
<h2 aria-live="polite">March 2026</h2>
<button aria-label="Next month">»</button>
</div>
<table role="grid" aria-label="March 2026">
<thead>
<tr>
<th scope="col" abbr="Mon">Mo</th>
<th scope="col" abbr="Tue">Tu</th>
<!-- ... -->
</tr>
</thead>
<tbody>
<tr>
<td tabindex="-1">1</td>
<td tabindex="-1">2</td>
<td tabindex="0" aria-selected="true">3</td>
<!-- ... -->
</tr>
</tbody>
</table>
</div>
Component Testing Checklist
For every interactive component, verify these fundamentals:
- Keyboard only: Can you operate the component entirely without a mouse? Can you reach it, interact with it, and leave it?
- Focus visible: Is the currently focused element always visually obvious?
- Focus order: Does focus move in a logical sequence?
- Screen reader: Does the component announce its name, role, state, and any changes? Test with at least NVDA or VoiceOver
- Zoom: Does the component remain usable at 200% and 400% zoom?
- Motion: Does the component respect
prefers-reduced-motion? - Color independence: Is information conveyed through means other than color alone?
WAI-ARIA Authoring Practices
The ARIA Authoring Practices Guide (APG), published by the W3C, is the authoritative reference for implementing accessible interactive components. It provides detailed design patterns, keyboard interaction models, and example implementations for every widget type covered in this article and many more.
The APG covers patterns including:
- Alert and alert dialog
- Breadcrumb
- Carousel (with auto-rotate)
- Checkbox (dual-state and tri-state)
- Combobox (with listbox, grid, or tree popups)
- Dialog (modal and non-modal)
- Disclosure (show/hide)
- Feed (infinite scrolling)
- Grid and data grid
- Listbox (single and multi-select)
- Menu and menu bar
- Meter, slider, and spin button
- Tabs
- Toolbar
- Tooltip
- Tree view and tree grid
Each pattern in the APG includes a description of the expected keyboard behavior, the required ARIA roles and attributes, and working code examples. When building custom components, always consult the APG first and follow its guidance precisely. Deviating from established patterns creates inconsistency that confuses assistive technology users who have learned to expect standard behaviors.
The APG is available at www.w3.org/WAI/ARIA/apg/ and is regularly updated alongside the ARIA specification.
Je váš web prístupný?
Skenujte svoj web zadarmo a získajte WCAG skóre za pár minút.
Skenovať web zadarmo