ARIA and Widget Patterns

WAI-ARIA is a W3C specification whose attributes (role and the aria-* attributes) tell assistive technology what a custom control is and what state it is in when HTML has no element for it. A tabs component has no native tab element, so the developer marks the container with role="tablist", each tab with role="tab", and the tab whose panel is shown with aria-selected="true". ARIA changes only what the accessibility tree exposes: the arrow-key behaviour users expect from tabs still has to be written in script.

The W3C ARIA Authoring Practices Guide (APG) describes how common widgets should behave: the roles, states and properties each one needs and the keys that operate it. For a tester, an APG pattern is a ready-made expected result: this lesson turns pattern pages into tests for five common widgets and into defects that cite the pattern and WCAG 4.1.2.

Why it matters

Custom widgets are among the easiest places for accessibility to break in modern web applications, because everything a native element provides must be rebuilt by hand: role, name, state, focus movement and every key. A dialog that lets focus escape to the page behind it, tabs whose selected state never changes, or a combobox whose highlighted option is never announced can stop a keyboard or screen reader user from finishing a payment or a form. These are failures of WCAG 2.2 success criteria such as 4.1.2 Name, Role, Value, 2.1.1 Keyboard and 2.4.3 Focus Order, all Level A.

Wrong ARIA can also be worse than none: a role promises behaviour, and when the behaviour is missing the user is misled. For the tester, the patterns turn a vague finding such as "the tabs are not accessible" into precise, checkable expectations a developer can act on, and they help separate real WCAG failures from deviations from a convention.

ARIA in one paragraph, and the first rule

The screen reader testing guide covers ARIA in depth; this is the short version this lesson builds on. Roles say what an element is (tab, dialog, menu, combobox). States are conditions that change while the user works (aria-expanded, aria-selected, aria-checked). Properties are more stable characteristics and relationships (aria-label, aria-labelledby, aria-controls, aria-haspopup). The accessible name is still computed as that guide describes, with aria-labelledby and aria-label taking precedence. Some roles only make sense in context: a tab belongs in a tablist, an option in a listbox, a menuitem in a menu.

The first rule of ARIA use: if a native HTML element or attribute already has the semantics and behaviour you need, use it instead of adding ARIA. A <button> beats <div role="button"> because it is focusable and handles Enter and Space by itself; <details> with <summary> is a ready-made disclosure; <dialog> opened with showModal() makes the rest of the page inert. So the first review question for any custom widget is whether it needs ARIA at all. Tabs, menu buttons and comboboxes with a custom list have no complete native equivalent, which is why their patterns matter to testers.

How to read an APG pattern page

Every APG pattern page is built from the same parts, and each part turns into a kind of test. Write the expected results down before you open the product.

For example, the Right Arrow entry of the Tabs pattern becomes a keyboard test: put focus on a tab, press Right Arrow, expect focus on the next tab, and on the last tab expect focus to wrap to the first. Its roles section becomes a tree check: every tab is a tab inside a tablist, and only the tab whose panel is shown has aria-selected="true".

  • About This Pattern: what the widget is for. Match a widget by behaviour, not looks: a row of buttons that switches panels in place is tabs, a "Show more" button is a disclosure, and a list of site links that drops down is navigation, not a menu.
  • Keyboard Interaction: one entry per key, each a test step with an expected result. Keys marked "(Optional)", such as Home and End for tabs, are not required, but when the product implements them they must work as described.
  • WAI-ARIA Roles, States, and Properties: roles and which one contains which, states that must change at runtime, and properties that name and connect the parts. Each line is a check in the accessibility tree. Read the conditions: "if the tab list has a visible label" decides between aria-labelledby and aria-label.
  • Examples: working demonstrations, each with a keyboard support table and a table of roles, properties, states and tabindex values. The tabindex column shows which focus technique the example uses.
  • Choices the pattern leaves open: automatic or manual activation of tabs, horizontal or vertical orientation. Ask the team which variant they chose and record it in the test basis; otherwise you cannot tell a defect from a design decision.
The APG Tabs pattern in markup: a named tablist, tabs whose aria-selected state and roving tabindex move together, and panels named by their tabs.

Two tabs of the order page, the first one selected

<div Part 1:role="tablist" Part 2:aria-label="Order details">
  <button Part 3:role="tab" id="t1" Part 4:aria-selected="true"
          Part 5:aria-controls="p1" Part 6:tabindex="0">Items</button>
  <button Part 3:role="tab" id="t2" Part 4:aria-selected="false"
          Part 5:aria-controls="p2" Part 6:tabindex="-1">Delivery</button>
</div>
<div Part 7:role="tabpanel" id="p1" Part 8:aria-labelledby="t1"
     Part 9:tabindex="0">…</div>
<div Part 7:role="tabpanel" id="p2" Part 8:aria-labelledby="t2"
     Part 9:tabindex="0" Part 10:hidden>…</div>
  1. 1Tab listContains every tab; the whole list is one Tab stop.
  2. 2Name of the tab listaria-labelledby when a visible label exists, aria-label otherwise.
  3. 3Tab role on each tabInside the tablist; a native <button> keeps Enter and Space.
  4. 4Selected stateExactly one true, on the tab whose panel is shown; it moves with the panel and the change is announced.4.1.2
  5. 5Tab points to its panelThe value is the panel’s id.
  6. 6Roving tabindexOnly the selected tab is in the Tab sequence; with automatic activation, Left and Right Arrow move focus, the 0 and the selection together, wrapping at the ends.
  7. 7Tab panelOne panel per tab.
  8. 8Panel named by its tabA user entering the panel hears which tab it belongs to.
  9. 9Panel in the Tab sequenceThe next Tab after the tab list lands here when the panel content starts with nothing focusable.
  10. 10Inactive panel hiddenOut of the accessibility tree and the Tab order until its tab is selected.

Keys: Tab enters the list at the selected tab and the next Tab goes to the panel; Left and Right Arrow move between tabs and wrap; Home and End are optional; with manual activation, Enter or Space shows the panel.

Verifying five patterns end to end

Verify each pattern in three passes, because each pass catches different defects. First the accessibility tree in the browser’s DevTools: role, name and states before and after every interaction. Then the keyboard alone: every key in the pattern, a visible focus indicator, and where focus lands when the widget opens or closes. Last a screen reader: what is announced on focus and whether each state change is announced. A widget can look right in the tree and still fail with the keyboard, or pass both and be announced poorly because screen readers support a role differently.

  • Modal dialog. Tree: role dialog named by its visible title, with aria-modal="true" on a custom dialog, or a native <dialog> opened with showModal(). Keyboard: focus moves into the dialog on open, Tab and Shift+Tab cycle inside it, Escape closes it, and focus returns to the control that opened it. Screen reader: the dialog role and name are announced on open, and reading with the arrow keys does not wander into the page behind.
  • Disclosure and accordion. Tree: a button whose aria-expanded matches what is visible; in an accordion each header button sits inside a heading. Keyboard: Enter and Space toggle the section, and collapsed content cannot be reached with Tab. Screen reader: "collapsed" becomes "expanded" when the button is pressed.
  • Tabs. Tree: a named tablist; tab elements with aria-controls and aria-selected; tabpanel elements named by their tab through aria-labelledby, with inactive panels hidden. Keyboard: the tab list is one Tab stop that lands on the active tab; Left and Right Arrow move between tabs and wrap at the ends; the next Tab enters the panel, which itself takes tabindex="0" when its content starts with nothing focusable. In a right-to-left layout, check the arrow direction your team recorded. Screen reader: each tab announces its name, role and selected state, usually with its position, such as 2 of 3.
  • Menu button. Tree: a button with aria-haspopup and, while the menu is open, aria-expanded="true", opening a menu of menuitem elements. Keyboard: Enter or Space opens the menu on its first item (Down Arrow may also), Up and Down Arrow move, Enter activates an item and closes the menu, Escape closes it and returns focus to the button. Screen reader: the button says it opens a menu, and each item is announced as a menu item.
  • Combobox. Tree: the input has role combobox, a name from its label, aria-expanded matching the list, aria-controls pointing to a listbox of option elements, and aria-activedescendant set to the id of the highlighted option, which has aria-selected="true". Keyboard: typing filters, Down Arrow moves through the options while focus stays in the input, Enter accepts an option, Escape closes the list. Screen reader: each highlighted option is read as you move, and typing still works.

Try it: HTML Formatter Try it: Test Case Generator

Roving tabindex and aria-activedescendant

Composite widgets such as tabs, menus, listboxes and grids are one Tab stop, and arrow keys move inside them. APG describes two ways to build this. With a roving tabindex, the current item has tabindex="0" and every other item tabindex="-1"; on each arrow key the script moves the 0 to the new item and calls focus() on it. DOM focus really moves, the browser scrolls the item into view, and if the user leaves with Tab and comes back, focus returns to the item they left.

With aria-activedescendant, DOM focus stays on one element (the container, or the input of a combobox) and the script sets the attribute to the id of the active item, which assistive technology then treats as focused. The script must draw the visual focus and scroll the item into view itself, because the browser does neither. A combobox needs this technique so the user can keep typing while moving through the options.

To tell them apart, open the Elements panel of Chrome or Edge DevTools and press the arrow keys: see which attribute changes. Then add document.activeElement as a live expression (the eye icon at the top of the Console), which keeps showing where DOM focus really is. Each technique breaks in recognisable ways:

  • Roving: several items keep tabindex="0", so Tab walks through every tab instead of moving to the panel; or, after a re-render, no item has 0 and the widget cannot be reached with Tab at all (2.1.1).
  • Roving: the arrow key moves the highlight but focus() is never called, so the focus indicator and the screen reader stay on the old item.
  • Roving: the 0 stays on the first item, so returning with Shift+Tab lands on a tab that is not the selected one.
  • Active descendant: the attribute points to an id that no longer exists, typically after a list is filtered and rebuilt; the highlight moves but the screen reader says nothing.
  • Active descendant: the attribute changes but no highlight is drawn, so sighted keyboard users cannot see where they are (usually reported under 2.4.7 Focus Visible); or the active option stays scrolled out of view.
  • Active descendant: the referenced element has no option or menuitem role, or lies outside the widget and the list it controls, so it is announced as plain text or not at all.

Worked example: testing order tabs and writing the defects

An order page has three tabs: Items, Delivery and Payment. In refinement the team chose the APG Tabs pattern with automatic activation, so that is the test basis. The three passes produce four findings, and each one is classified before it is reported.

A useful ARIA defect names the component and the pattern variant, gives the exact key sequence, states the expected and actual role, name or state as the tree shows it and as the screen reader announces it, and cites a WCAG criterion only when the product really fails it. Here are the four findings, classified, followed by the report for the second one:

  • Tree: the container is a plain <div>, so the three tabs sit under a generic node with no tablist role and no name. WAI-ARIA requires a tab to be contained in a tablist; the grouping users see is not exposed, which fails 1.3.1 Info and Relationships, the criterion the W3C ACT rule "ARIA required context role" maps it to.
  • Tree and screen reader: after Right Arrow to Delivery, the Delivery panel is shown but aria-selected="true" stays on Items, and the screen reader does not announce Delivery as selected. The state on screen is not the state exposed: a 4.1.2 failure, rated Major, because a blind user cannot tell which panel they are reading.
  • Keyboard: every tab has tabindex="0", so Tab visits all three tabs before reaching the panel, and Right Arrow on Payment does nothing instead of wrapping to Items. Every tab still works from the keyboard, so 2.1.1 is met: report a deviation from the APG keyboard interaction, rated Minor, without a WCAG criterion.
  • Tree: the panels have the tabpanel role but no aria-labelledby, so a panel is entered without hearing which tab it belongs to. WAI-ARIA 1.2 requires a name for a tabpanel, so report it against the pattern and WAI-ARIA, rated Minor.
Title:     Order tabs: selected state stays on "Items" after moving to "Delivery"
Component: Order details tabs, /orders/1042 · APG Tabs, automatic activation
Env:       Windows 11 · Firefox · NVDA (versions in the attachment)
Steps:     1. Tab to the "Items" tab.  2. Press Right Arrow.
Expected:  Focus, panel and selection move to "Delivery":
           aria-selected="true" on Delivery and "false" on Items;
           the screen reader announces Delivery as a selected tab.
Actual:    The Delivery panel is shown, but aria-selected="true" stays on Items;
           NVDA does not announce a selected state.
Criterion: WCAG 2.2 4.1.2 Name, Role, Value (Level A)
Severity:  Major
Evidence:  accessibility pane screenshots before and after; DOM snippet

Try it: AI Bug Description Generator

Common mistakes

  • Adding ARIA where a native element would do, then testing the ARIA instead of asking why it is there. <div role="button"> needs focus and Enter and Space handling that <button> already has.
  • Checking only that the roles exist. Roles are static; most defects are states that do not change, such as aria-expanded and aria-selected, and focus that does not move. Check the tree before and after every key.
  • Treating every difference from APG as a WCAG failure. WCAG requires keyboard operation and an exposed name, role and value, not the APG key bindings: tabs operated with Tab instead of arrows deviate from the pattern but can still meet 2.1.1.
  • Trusting the visible highlight in an aria-activedescendant widget. The highlight and the attribute are set separately, so confirm the attribute points to an existing element with the right role.
  • Citing 4.1.1 Parsing for invalid ARIA in a WCAG 2.2 audit. WCAG 2.2 marks 4.1.1 as obsolete and removed; cite 4.1.2, 1.3.1 or 2.1.1 according to what users actually lose.

The QA perspective

On a team, the pattern conversation belongs before the build. When a story introduces a custom widget, ask whether a native element would do, and if not, which APG pattern and variant it follows. Put the pattern name and the variant into the acceptance criteria, so designers, developers and testers share one expected result and the pattern’s keyboard table becomes part of the definition of done.

When you test, keep a checklist per component built from its pattern page and run it in the three passes, recording the browser and screen reader pairing with versions. Report each finding with the pattern rule it breaks and, separately, the WCAG criterion it fails, if any, so triage can tell conformance failures from consistency issues. Give components in a shared design system the most attention: one broken combobox in the library is the same defect on every page that uses it, and one fix repairs them all.

Practice exercise

Verify a city combobox and report what fails

Your task

A delivery address form has a City field built as an editable combobox that follows the APG Combobox pattern with a listbox popup. Its markup when the page loads is below. While testing with the keyboard and a screen reader you observe: (1) Tab reaches the field and the screen reader announces the name City and a combobox role; (2) typing "Da" shows a list with one city, Dammam, but aria-expanded stays "false"; (3) Down Arrow highlights Dammam and focus stays in the input, but aria-activedescendant is set to "opt-3" while the rebuilt list now contains only <li id="opt-1" role="option">Dammam</li>, and the screen reader says nothing; (4) Enter puts Dammam in the field and closes the list, and Escape closes the list when it is open.

Decide which focus technique the widget uses and how you can confirm it. Classify each observation as pass or fail, name the WCAG criterion or pattern rule for each failure, and write a defect title for each failure.

Material
<label for="city">City</label>
<input id="city" role="combobox" aria-autocomplete="list"
       aria-expanded="false" aria-controls="city-list">
<ul id="city-list" role="listbox" hidden>
  <li id="opt-1" role="option">Riyadh</li>
  <li id="opt-2" role="option">Jeddah</li>
  <li id="opt-3" role="option">Dammam</li>
</ul>
Steps
  1. List the roles, states and properties the APG Combobox pattern expects for this markup.
  2. Identify the focus technique from where DOM focus stays and which attribute changes.
  3. Compare each observation with the pattern’s keyboard and attribute rules.
  4. Classify each failure: a WCAG criterion, or a pattern deviation only.
  5. Write one defect title per failure that names the component, the broken state and its effect.
Show the model answer
Model answer

Expected for this markup (step 1): the input has role combobox and its name from the <label>; aria-expanded is "false" while the list is hidden and "true" while it is shown; aria-controls points to the listbox city-list of option elements; aria-autocomplete="list" matches the filtering; while an option is highlighted, aria-activedescendant holds that option’s id and the option has aria-selected="true"; DOM focus stays in the input.

Technique: aria-activedescendant. While Down Arrow moves the highlight, document.activeElement stays the input and only the attribute changes. That is what the pattern expects for a combobox, so the choice of technique is right; its implementation is not.

Observations 1 and 4 pass: the name comes from the <label>, the role is combobox, Enter accepts the option and closes the list, and Escape closes the open list.

Observation 2 fails: aria-expanded must be "true" while the list is shown, otherwise screen reader users are not told that suggestions appeared; WAI-ARIA 1.2 lists aria-expanded as required for a combobox. WCAG 2.2 4.1.2 Name, Role, Value (state not exposed). Title: "City combobox: aria-expanded stays false while the suggestion list is shown".

Observation 3 fails: after filtering, the attribute points to an id that no longer exists, so assistive technology has no active option and the highlighted city is never announced. This is 4.1.2 again, rated at least Major, because the user cannot hear which city they are about to accept. Title: "City combobox: highlighted city is not announced; aria-activedescendant points to a removed option id after filtering". Suggest stable ids per city, or updating the attribute after every render, and retest with filters that change the length of the list.

Frequently asked questions

Is the APG a standard a product can fail?

No. The APG is informative W3C guidance that shows how to apply WAI-ARIA; WCAG 2.2 is the standard conformance is measured against. Use APG patterns as the expected behaviour in test cases, cite a WCAG criterion only when users lose what it requires, and report other differences from the pattern as consistency or usability defects.

Should tabs activate on focus or only on Enter?

APG describes both. It recommends automatic activation when the panel can be shown without noticeable delay, and manual activation, with Enter or Space, when showing a panel is slow. Either is correct if the team chose it deliberately and applies it consistently, so record the choice in the acceptance criteria.

Which is better: roving tabindex or aria-activedescendant?

Neither in general. Roving tabindex moves real focus, so the browser scrolls to and styles the focused item. aria-activedescendant keeps focus on one element, which a combobox needs so the user can keep typing. Test whichever the widget uses against its own failure signs.

Do I still need a screen reader if the accessibility tree looks right?

Yes, with at least one pairing. The tree shows what the browser exposes; only a screen reader shows what users hear, including state changes that are not announced and differences in how screen readers support a role.

References

  • ARIA Authoring Practices Guide (APG)W3C Web Accessibility Initiative (WAI)Patterns for common widgets (tabs, menus, dialogs, comboboxes) with the expected keyboard interaction and ARIA usage — the reference testers compare custom components against.
  • Accessible Rich Internet Applications (WAI-ARIA) 1.2W3C · 2023The W3C Recommendation that defines ARIA roles, states and properties — how custom widgets expose their meaning to assistive technology.
  • Web Content Accessibility Guidelines (WCAG) 2.2W3C · 2024The W3C Recommendation that defines the testable success criteria for accessible web content, organised under four principles and three conformance levels (A, AA, AAA).
  • Understanding WCAG 2.2W3C Web Accessibility Initiative (WAI)The informative explanation of each WCAG 2.2 guideline and success criterion: intent, who benefits, examples and techniques (not normative).

Practise with Mutqan tools

Beautify HTML with proper indentation of nested elements, preserving pre, script and style blocks.

Formatting

Generate structured test cases (id, title, preconditions, steps, expected result, priority) from a feature description and field list.

Test Design

Generate a tailored QA checklist (functional, UI, API, security, performance, accessibility, release) for any feature with Mutqan AI.

Test Generation AI

Turn rough notes into a complete bug report: steps to reproduce, expected vs actual, environment, severity and attachments checklist.

Bug Analysis AI

Go deeper

Your progress on this topic

Part of the QA Roadmap