Skip to main content

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:

  • Escape closes the dialog
  • Tab moves forward through focusable elements inside the dialog, wrapping from last to first
  • Shift+Tab moves backward, wrapping from first to last

Required ARIA attributes:

  • role="dialog" on the modal container (or role="alertdialog" for urgent confirmations)
  • aria-modal="true" to indicate that content behind the dialog is inert
  • aria-labelledby pointing to the dialog's heading, providing an accessible name
  • aria-describedby optionally 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 and scope="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" and aria-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">&laquo;</button>
  <button aria-label="Next slide">&raquo;</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:

  • Enter or Space on a focused accordion header toggles the associated panel between expanded and collapsed
  • Arrow Down moves focus to the next accordion header
  • Arrow Up moves focus to the previous accordion header
  • Home moves focus to the first accordion header
  • End moves focus to the last accordion header

Required ARIA attributes:

  • Each accordion header should be a <button> (or have role="button") wrapped in an appropriate heading level
  • aria-expanded="true" or "false" on the button to indicate the current state of the panel
  • aria-controls on the button, pointing to the id of the associated panel
  • The panel region can use role="region" with aria-labelledby pointing 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 elements
  • role="tab" on each tab trigger element
  • role="tabpanel" on each content panel associated with a tab

Required ARIA attributes:

  • aria-selected="true" on the active tab, "false" on all others
  • aria-controls on each tab, pointing to its associated tabpanel's id
  • aria-labelledby on each tabpanel, pointing to its associated tab's id
  • Only the active tab should be in the tab order (tabindex="0"); inactive tabs should have tabindex="-1"

Keyboard interaction:

  • Arrow Left / Arrow Right moves focus between tabs (horizontal tab list)
  • Arrow Up / Arrow Down for vertical tab lists
  • Home moves focus to the first tab
  • End moves focus to the last tab
  • Tab moves 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") and aria-expanded to indicate the menu's open state
  • The menu container uses role="menu"
  • Each menu item uses role="menuitem"
  • For checkable items, use role="menuitemcheckbox" or role="menuitemradio"

Keyboard interaction:

  • Enter, Space, or Arrow Down on the trigger button opens the menu and focuses the first item
  • Arrow Up on the trigger opens the menu and focuses the last item
  • Arrow Down / Arrow Up navigates between menu items
  • Enter or Space activates the focused menu item
  • Escape closes 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
  • Home moves focus to the first item, End to 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 Escape key 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 input
  • aria-autocomplete set to "list" (suggestions shown), "inline" (autocomplete text appears in the input), or "both"
  • aria-expanded="true" when the suggestion list is visible, "false" when hidden
  • aria-controls pointing to the suggestion list's id
  • aria-activedescendant on the input, pointing to the id of the currently highlighted option — this lets screen readers announce the focused option while keeping DOM focus in the text input
  • role="listbox" on the suggestion list
  • role="option" on each suggestion, with aria-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 Up navigates through suggestions
  • Enter selects the highlighted suggestion
  • Escape closes 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" or role="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">&times;</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 Right moves to the next day
  • Arrow Left moves to the previous day
  • Arrow Down moves to the same day in the next week
  • Arrow Up moves to the same day in the previous week
  • Home moves to the first day of the current week
  • End moves to the last day of the current week
  • Page Up moves to the previous month
  • Page Down moves to the next month
  • Shift+Page Up moves to the previous year
  • Shift+Page Down moves to the next year
  • Enter or Space selects the focused date

Essential ARIA:

  • aria-label on 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 have tabindex="-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">&laquo;</button>
    <h2 aria-live="polite">March 2026</h2>
    <button aria-label="Next month">&raquo;</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