The four patterns you keep rebuilding
Combobox, disclosure, dialog and tree cover most of what an agentic surface needs, and the APG specifies each down to the keystroke — including why a combobox reaches for aria-activedescendant where a tree can use a roving tabindex.
You will not build a hundred custom widgets. You will build four, over and over, in different clothes. The field picker on the review gate is a combobox. The “show the raw extraction” toggle is a disclosure. The confirmation that appears before a destructive correction is a modal dialog. The collapsible run trace is a tree.
That these four are the ones worth learning first is this course’s own judgment about agentic surfaces, arrived at by looking at what those surfaces actually contain. The Authoring Practices Guide publishes roughly two dozen patterns and ranks none of them. It has no opinion about which matter to you. The useful thing it does have is a keyboard table and an attribute list for each one, which turns the promise you signed in the previous lesson into a finite, checkable list.
Disclosure: the one that is almost free
Start here because it is the cheapest and because people still get it wrong. “A disclosure is a widget that enables content to be either collapsed (hidden) or expanded (visible). It has two elements: a disclosure button and a section of content whose visibility is controlled by the button.”
- Role: the control is a
button. Use the native element and you have already met most of the pattern. - State:
aria-expandedistruewhen the content is visible andfalsewhen it is hidden. Not absent when collapsed. Absent means “not an expandable thing.” - Optional:
aria-controlsreferring to the controlled section. - Keys: Enter and Space both activate the control and toggle visibility. A native
buttongives you both.
The recurring failure is a chevron that rotates with no aria-expanded anywhere, which reports the state to sighted users and to nobody else. That is 4.1.2 Name, Role, Value at Level A, and the fix is one attribute.
Dialog: the role that changes what the rest of the page is
The APG opens with a sentence people skim past: “A dialog is a window overlaid on either the primary window or another dialog window. Windows under a modal dialog are inert.” Inert is a claim about the whole rest of the page, not about the box you drew.
- Roles and properties:
role=“dialog”on the container,aria-modal=“true”, an accessible name fromaria-labelledbypointing at the visible title or fromaria-label, and optionallyaria-describedbyfor a simple description. - Keys: Tab moves to the next tabbable element and wraps to the first when it is on the last; Shift+Tab moves back and wraps to the last when it is on the first; Escape closes the dialog.
Note what that Tab behaviour implies. Wrapping is not something the browser does for you because a role is present. Containing the tab sequence is code you write, which is the previous lesson’s point arriving with a bill attached. The full focus lifecycle for an overlay, and the criterion that people misquote when they argue about it, is the focus-in-an-overlay lesson.
Combobox: why focus refuses to move
“A combobox is an input widget that has an associated popup. The popup enables users to choose a value for the input from a collection.” The popup can be a listbox, a grid, a tree or a dialog, and the pattern page gives a keyboard table for each. Listbox is the implicit one, which is why aria-haspopup only needs writing when it is one of the others.
- Properties:
aria-expandedfalsewhen the popup is hidden andtruewhen it is visible;aria-controlsreferencing the popup element;aria-activedescendantset when a descendant of the popup is the active option;aria-autocompleteasnone,listorboth. - Keys on the combobox: Down Arrow moves focus into the popup when one is available, landing on the first focusable element or on an auto-selected suggestion. Alt+Down Arrow displays the popup without moving focus. Escape dismisses a visible popup, and may optionally clear the input if the popup is already hidden. Enter accepts an autocomplete suggestion or performs the default action. Alt+Up Arrow returns focus to the combobox or closes the popup.
Now the mechanism worth understanding rather than memorising. The pattern states that “DOM focus is maintained on the combobox and the assistive technology focus is moved within the listbox using aria-activedescendant.” DOM focus cannot leave the text input, because if it did the user could no longer type, and typing is what a combobox is for. So the widget needs a second notion of “which option is active” that lives entirely in an attribute. That is what aria-activedescendant is: a pointer, by id, to the element the assistive technology should treat as current while real focus stays put.
Tree: the same problem, solved the other way round
- Structure: a container with
role=“tree”, each noderole=“treeitem”, and each collection of children inside an element withrole=“group”. - State:
aria-expandedon every parent node;aria-selectedoraria-checkedfor selection. Where the full node set is not in the DOM, each node also carriesaria-level,aria-setsizeandaria-posinsetso the position can still be announced. - Keys: Up and Down Arrow move between adjacent visible nodes. Right Arrow expands a collapsed parent, then moves to its first child. Left Arrow collapses an expanded node, then moves to its parent. Home and End jump to the first and last node. Enter activates. Typing characters moves to the next node whose label starts with them. An optional asterisk expands the siblings at the current level. In a multi-select tree, Space toggles selection and Shift and Control extend it.
Here is the sentence that gets stated backwards more than any other in this pattern. The APG says the “tree role supports the aria-activedescendant property, which provides an alternative to moving DOM focus among treeitem elements when implementing keyboard navigation.” Read that carefully. Moving DOM focus is the baseline the sentence is written from, and aria-activedescendant is offered as the alternative. A tree does not require a roving tabindex and the guide does not forbid aria-activedescendant here. Both are available, and the framing is the reverse of the combobox, where DOM focus has to stay pinned to the input.
How to read a pattern page in four minutes
Every pattern page has the same four parts, and reading them in this order is faster than reading top to bottom:
- The definition. One or two sentences. Use it to confirm you have the right pattern before you spend time on the rest. Half of all bad ARIA is the right pattern applied to the wrong widget.
- The keyboard table. This is the acceptance criteria. Copy it into your test file more or less verbatim.
- The roles, states and properties list. This is what has to stay truthful at runtime, which is a harder problem than setting it correctly once.
- The examples. Working implementations with the accessibility tree shown. Read one before you write your own version of the same thing.
And keep the standing caveat in view. These pages are recommended implementations. The obligation you cite in a finding is a WCAG criterion number; the APG is what you cite for how the W3C suggests meeting it.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
From memory: give the container and item roles for a tree, the attribute that carries expand state, and the two attributes that let a combobox and a tree track an active item differently.
Check your answer
- Tree structure:
role=“tree”on the container,role=“treeitem”on each node,role=“group”on each collection of children. - Expand state:
aria-expanded, on parent nodes in a tree and on the control in a disclosure. Same attribute, two patterns. - Active item: a combobox keeps DOM focus on the input and points at the active option with
aria-activedescendant. A tree can move DOM focus with a rovingtabindex, and the APG namesaria-activedescendantas the alternative.
If you also produced aria-level, aria-setsize and aria-posinset, those are the three that matter most in an agent trace, because a lazily loaded run rarely has every node in the DOM.
Hands on
Name every widget in the review gate after a pattern
Done when: Every interactive widget in the review gate is labelled with an APG pattern name or explicitly marked as a native control needing none, and each labelled widget has a keyboard table copied from its pattern page with a pass or fail beside every row.
- List every interactive widget in the gate. For each, write the pattern name from the APG, or write “native, no pattern needed” and move on. A widget you cannot name is the interesting one; open the pattern index and find the closest match before you invent something.
- For each named widget, copy the keyboard table off its pattern page into your notes, one row per key. Do not summarise it. The summary is where the missing key hides.
- Put your hands on the keyboard and walk every row. Mark pass or fail. Test Escape and Space specifically, because those are the two that get missed and Space is the one that scrolls the page when nothing catches it.
- For any composite widget, check the single-tab-stop rule: Tab should move past the whole thing in one press once you are done with it. Count the presses and write the count down.
- Turn the failures into
CONFORMANCE.mdrows. A missing key on an interactive control is 2.1.1 Keyboard, Level A; a state the widget shows visually and never exposes is 4.1.2 Name, Role, Value, Level A. Cite the APG pattern in the test column as the implementation you checked against, in its own clause, so nobody reads it as the source of the obligation.
What this does not cover
The dialog section above stopped at keys and attributes and deliberately left out what happens to focus when the thing opens and closes. That is the whole of the focus-in-an-overlay lesson, along with 2.1.2 No Keyboard Trap, Level A, which almost everyone cites incorrectly when defending a modal.
Nothing here has been listened to. Every check in the hands-on was made with your eyes and your fingers, which catches missing keys and misses every wrong name. The screen-reader lesson closes that gap and is where the interoperability testing the guide calls essential actually happens.
And the tree section is a down payment. Building an agent run trace as a real tree, with the level and set-size attributes doing work on a lazily-loaded run, is the trace-tree lesson in the next module.
Read this next — primary source
ARIA Authoring Practices Guide: Combobox PatternW3C Web Accessibility Initiative — free, and non-normative. Guidance about applying WAI-ARIA, not the specification.
The densest of the four pattern pages and the one whose structure teaches you how to read the other three. It separates the popup variants, gives a keyboard table per variant, states which attributes carry which state, and says outright that DOM focus stays on the combobox while assistive technology focus moves with aria-activedescendant. Read the whole page including the examples, because the examples are where the attribute rules stop being abstract. Everything you learn about reading this page transfers directly to the tree, dialog and disclosure pages.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.