Where Practice Diverges from the APG

Field Notes

The WAI-ARIA APG is a starting point for implementation, not an absolute rule. Major UI libraries sometimes choose to intentionally deviate from what the APG says, based on real-world testing with screen readers.

This page collects those intentional divergences, together with what the APG specifies, what practice actually does, the reasoning behind it (background and test results), the libraries that adopt it, and sources. Follow a pattern name to its detail page.

Dialog (Modal)

Not trusting aria-modal="true" alone — hiding the background with aria-hidden / inert

The APG says
A modal dialog should have role="dialog" and aria-modal="true" — an ARIA 1.1 addition meant to replace the older technique of applying aria-hidden to all background elements.
What practice does
floating-ui's FloatingFocusManager and React Aria's ariaHideOutside walk the outside DOM tree while a modal is open and apply aria-hidden (or inert where possible). Base UI's Dialog / AlertDialog build on the same machinery; many such implementations never set aria-modal at all.
Why
It started with measured distrust of aria-modal support. A WebKit bug (reported 2017, unresolved for years) made VoiceOver skip static text inside aria-modal dialogs; Bootstrap and react-modal issues documented iOS VoiceOver swiping straight through focus traps into the background. aria-modal alone works in NVDA but not reliably in VoiceOver — so explicitly hiding the background with aria-hidden became the field standard that closes the gap across ATs, while fully meeting the APG intent: background hidden, focus contained, focus restored.

Adopted by:floating-uiReact AriaBase UI

Sources:WebKit Bug 174667 — aria-modal makes VoiceOver skip content(opens in a new tab)Floating UI — FloatingFocusManager(opens in a new tab)react-modal#611 — VoiceOver fails with aria-modal(opens in a new tab)

Native <dialog> went from "avoid" to "acceptable with conditions"

The APG says
The APG dialog pattern is written as a role="dialog" + JavaScript custom widget and does not take a position on the native <dialog> element.
What practice does
Scott O'Hara's "Having an open dialog" (2019) — long the canonical reason to avoid native dialog — was revised in January 2023 to a conditional endorsement. Meanwhile staple libraries like a11y-dialog still decline to adopt it, citing cross-browser styling differences and the missing open event.
Why
In 2019 only Chromium supported it, with measured problems: broken initial focus, inconsistent screen reader behavior, focus not returning on close. Once Safari 15.4 and Firefox 98 shipped support in 2022, the author updated to a conditional endorsement — keep robust custom implementations until user browser statistics catch up. A textbook case of recommendations changing on a measurable criterion (browser support reach) rather than opinion; that showModal() provides aria-modal semantics automatically also feeds the decision.

Adopted by:Scott O'Haraa11y-dialog

Sources:Scott O'Hara — Having an open dialog(opens in a new tab)a11y-dialog — On the dialog element(opens in a new tab)

Focus traps converged on invisible sentinel elements, not keydown interception

The APG says
The APG text specifies only the behavior — Tab wraps from last to first — and leaves the mechanism (sentinels vs. keydown handling) to the developer.
What practice does
The APG's own reference implementation (dialog.js) dynamically inserts tabindex="0" divs before and after the dialog and bounces focus back from them. Radix UI (react-focus-guards) inserts two hidden spans at the document edges; floating-ui's FloatingFocusManager generates the same hidden guards (enabled by default). The industry converged on the same mechanism independently.
Why
Intercepting Tab keydown alone cannot catch focus escaping by other means — mouse clicks, or a screen reader's virtual cursor. Focus-event-based sentinels catch departures regardless of how focus moved. This is convergence rather than conflict: on an implementation detail where the APG is silent, its reference code and the major libraries reached the same answer — and it is the mechanism to choose when hand-rolling a focus trap.

Adopted by:W3C APG reference implementationRadix UIfloating-ui

Sources:w3c/aria-practices#545 — Documenting the sentinel technique(opens in a new tab)W3C APG — Dialog (Modal) Pattern(opens in a new tab)Floating UI — FloatingFocusManager(guards)(opens in a new tab)

Alert and Message Dialogs

Placing initial focus on the safe action is the consumer’s job, not the library’s

The APG says
In confirmation dialogs for destructive actions, initial focus should land on the least destructive control, such as Cancel.
What practice does
The AlertDialog in headless libraries (Base UI, Radix UI, React Aria) focuses the first focusable element by default and makes no attempt to identify the "safe" button. Instead they expose props like initialFocus and leave the policy to the consumer.
Why
Which button is "safe" depends on DOM order, label wording, and application context — a library cannot know. So headless libraries guarantee the mechanics (moving, trapping, and restoring focus) and expose the policy as an API. Meeting the APG requirement is the consumer’s implementation: point initialFocus at the Cancel button, or place the safe control first in the DOM.

Adopted by:Base UIRadix UIReact Aria

Sources:Radix UI — Alert Dialog(opens in a new tab)Base UI — Alert Dialog(opens in a new tab)

Menu and Menubar

Do not use menu / menubar roles for site navigation

The APG says
The menu / menubar pattern defines an application-style menu widget with a full set of custom keyboard conventions: Enter, Space, arrow keys, and more.
What practice does
The field consensus is to build site navigation as a <nav> with nested <ul><li><a> link lists and no menu roles. Even dropdown navigation is built with the disclosure pattern — a button with aria-expanded.
Why
Menu roles switch screen readers into an application interaction mode, forcing custom key handling that plain page browsing never needed. Worse, developers copy-paste incomplete implementations — observed failures include navigation becoming unusable because arrow-key handling was half-done. Following ARIA's first rule (don't use ARIA when HTML suffices), nested link lists are semantically sufficient — Adrian Roselli's conclusion, now shared broadly among practitioners. Menu / menubar is appropriate only for true application menus like the Google Docs menu bar.

Adopted by:Adrian RoselliHeydon PickeringTerrill Thompson

Sources:Adrian Roselli — Don't Use ARIA Menu Roles for Site Nav(opens in a new tab)Adrian Roselli — Link + Disclosure Widget Navigation(opens in a new tab)

Combobox

aria-selected marks committed selection, not the moving highlight

The APG says
In the APG combobox examples, aria-selected="true" moves together with aria-activedescendant: each arrow-key highlight also marks that option as selected.
What practice does
React Aria and Base UI apply aria-selected only to the committed value. The keyboard highlight is conveyed by aria-activedescendant (and visual styling) alone; aria-selected="true" appears only once the user commits, e.g. with Enter.
Why
Focus ("the option I am pointing at") and selection ("the value I committed") are different concepts. Moving aria-selected with the highlight makes screen readers announce "selected" on every arrow key, misleading users into thinking a value was committed — and in multi-select UIs the meaning of "selected" becomes ambiguous. Since aria-activedescendant already conveys focus, the field practice reserves aria-selected for actual selection state.

Adopted by:React AriaBase UI

Sources:React Aria — useComboBox(opens in a new tab)Base UI — Combobox(opens in a new tab)

On mobile, drop role="combobox" for a role="searchbox" inside a tray

The APG says
The input has role="combobox" tied to the listbox via aria-expanded / aria-controls, with arrow keys as the primary way to move between options.
What practice does
React Aria's ComboBox replaces the outer input with a button on mobile; tapping opens a full-height tray containing a search input — with role="searchbox", not combobox.
Why
Device testing showed that keeping role="combobox" on the tray input made screen readers announce "double tap to close" incorrectly. Touch screen reader users also have no physical arrow keys — the APG's primary navigation model simply does not exist for them. Combined with the state-management complexity of duplicated inputs, the team designed mobile as a different UI. A canonical case of prioritizing per-input-method usability over pattern fidelity.

Adopted by:React AriaReact Spectrum

Sources:React Aria Blog — Creating an accessible autocomplete experience(opens in a new tab)

The listbox cannot live next to the input — portals plus dynamic aria-hidden fill the gap

The APG says
APG examples place the listbox as a DOM sibling of the combobox, assuming screen readers can move naturally between input and list.
What practice does
React Aria portals the listbox to the end of the document to escape overflow clipping. To restore the broken assumption it walks the DOM with a TreeWalker, applies aria-hidden to everything except the input and listbox, and keeps watching mutations with a MutationObserver while open.
Why
Real apps put comboboxes inside scroll containers where an adjacent listbox gets clipped. Portalling fixes the visual problem but breaks reading order by putting DOM distance between input and list. React Aria's answer — temporarily hiding everything else from assistive technology — recreates the screen reader experience the APG structure was designed to produce, by other means. Porting the intended experience, not the textbook DOM.

Adopted by:React AriaReact Spectrum

Sources:React Aria Blog — Creating an accessible autocomplete experience(opens in a new tab)

Spinbutton

NumberField does not use role="spinbutton"

The APG says
Custom numeric inputs should have role="spinbutton" with aria-valuenow / aria-valuemin / aria-valuemax, and support incrementing and decrementing via the Up/Down arrow keys.
What practice does
The NumberField in React Aria and Base UI does not use the spinbutton role. Instead it renders a type="text" input (inputMode="numeric") with an aria-roledescription such as "Number field". Arrow-key stepping still works, but assistive technologies see a described text input.
Why
React Aria's docs say the component "follows the spinbutton ARIA pattern" — and in the same breath that it "works around bugs in VoiceOver with the spinbutton role". Their i18n blog post further explains choosing input type="text" + inputMode + a custom role description over type="number" to support currencies, percentages, and units. It is a measured workaround: using the APG role at face value breaks on real screen readers. Base UI adopts the same construction.

Adopted by:React AriaBase UI

Sources:React Aria — useNumberField(Accessibility features)(opens in a new tab)React Aria Blog — How we internationalized our number field(opens in a new tab)Base UI — Number Field(opens in a new tab)

Tabs

MUI does not default to the automatic activation the APG recommends

The APG says
The APG recommends that tabs activate automatically on focus as long as the associated panel displays without noticeable delay — automatic is the default, manual the fallback for slow panels.
What practice does
Radix, React Aria, and Headless UI default to automatic per the APG, but MUI (Material UI) defaults to manual activation — you must opt in to automatic with the selectionFollowsFocus prop.
Why
MUI's docs state that manual activation is often the preferable behavior, choosing a conservative default that avoids expensive re-renders and accidental switches. Across libraries the shared decision rule is the same: if switching a tab triggers heavy work (data fetching, large re-renders), go manual. The practical reading of the APG is to notice its own condition — "as long as panels display without noticeable delay".

Adopted by:MUI (Material UI)

Sources:W3C APG — Tabs Pattern(opens in a new tab)MUI — React Tabs(opens in a new tab)Radix Primitives — Tabs(opens in a new tab)

Grid

Avoid the grid pattern outside spreadsheet-like editing — prefer a plain <table>

The APG says
Interactive tables needing cell-level selection and two-dimensional arrow-key navigation should use the grid / treegrid pattern, with roving tabindex making the whole table one tab stop.
What practice does
Adrian Roselli says to ignore the grid role unless you are recreating spreadsheet-class editing — a table that merely contains links or buttons should stay a plain <table>. Sarah Higley frames the decision as interaction-first vs. information-first, showing concrete cases where visually grid-like UIs should not become ARIA grids.
Why
Declaring role="grid" obligates you to a full extra keyboard layer — 2D arrow navigation, managed tab stops — and incomplete implementations ship constantly. User testing surfaced confusion at unexpected arrow-key behavior and mismatches between responsive layouts and arrow-navigation column semantics. Upgrading a table to a grid just because rows are clickable is over-engineering; a plain table also announces column headers more naturally. Both practitioners reach this conclusion from testing, not theory.

Adopted by:Adrian RoselliSarah Higley

Sources:Adrian Roselli — ARIA Grid As an Anti-Pattern(opens in a new tab)Sarah Higley — Grids Part 1: To grid or not to grid(opens in a new tab)

React Aria's Calendar deliberately prunes grid navigation

The APG says
The grid pattern makes the entire grid — including row and column headers — navigable with arrow keys.
What practice does
React Aria's Calendar follows the grid pattern but skips the weekday header row in screen reader navigation, and adds a visually hidden "next" button so users can advance months without trekking back to the first cell.
Why
On mobile screen readers, where users step through elements sequentially, walking the full APG grid proved verbose and inefficient in testing. Skipping headers and adding the hidden button prioritize real traversal efficiency over pattern completeness. For the same reason, date entry uses individually focusable segments per unit rather than the free-form text field the APG assumes.

Adopted by:React AriaReact Spectrum

Sources:React Aria Blog — Date and Time Pickers for All(opens in a new tab)

Tooltip

Treating tooltips as visual-only hints — no role="tooltip", no aria-describedby

The APG says
The tooltip element should have role="tooltip" and be referenced from the trigger via aria-describedby, appearing on both hover and focus and dismissible with Escape. Touch input is not addressed at all.
What practice does
Base UI's Tooltip sets neither role="tooltip" nor aria-describedby; its docs state tooltips are visual aids and essential information belongs directly on the trigger (e.g. aria-label). Tooltips are disabled on touch devices, with Popover (openOnHover) recommended for essential content. Material UI guards against accidental touch triggering with enterTouchDelay (700ms long-press by default).
Why
Tooltips are designed around hover and focus — neither exists on touch devices, where a tap activates immediately. Information that lives only in a tooltip is structurally unreachable for some users. Sarah Higley flagged this incompatibility in 2019 and the spec side has not resolved it; Radix, which follows the APG faithfully, keeps receiving "tooltip does not open on touch" issues. Base UI codified a decision rule — is the trigger's purpose the open/close itself? — and confines tooltips to redundant visual hints.

Adopted by:Base UIMaterial UI

Sources:Base UI — Tooltip(Accessibility guidelines)(opens in a new tab)Sarah Higley — Tooltips in the time of WCAG 2.1(opens in a new tab)MUI — Tooltip(opens in a new tab)

Ariakit defaults to aria-labelledby instead of aria-describedby

The APG says
The trigger references the tooltip via aria-describedby — the tooltip is supplementary description, not the source of the accessible name.
What practice does
Ariakit v2 changed the default association to aria-labelledby. Consumers opt in to aria-describedby only when the tooltip is genuinely a description.
Why
In the wild, tooltips overwhelmingly label icon-only buttons that have no other accessible name. With aria-describedby such a button has no name at all; the maintainer verified inconsistent announcements across screen readers (VoiceOver duplicating "Undo, button, Undo" versus NVDA). aria-labelledby announces consistently, so Ariakit chose real-world usage over the APG default — while openly acknowledging the divergence and the remaining debate about dropping role="tooltip" entirely.

Adopted by:Ariakit

Sources:Ariakit — Tooltip(opens in a new tab)ariakit/ariakit#2228 — Tooltip accessibility(opens in a new tab)

The APG tooltip pattern itself lacks consensus — the field reaches for toggletips

The APG says
The APG tooltip pattern is explicitly marked "work in progress" without task force consensus — unresolved since 2016. The click-toggled "toggletip" does not exist in the APG pattern list at all.
What practice does
Sarah Higley calls role="tooltip" the "unloved child of roles" and recommends tooltips only for non-essential supplements. Heydon Pickering proposes the toggletip — a click-toggled info box that works across touch, mouse, and keyboard — as the replacement.
Why
Beyond the structural hover/focus flaw, WCAG 2.1's 1.4.13 (Content on Hover or Focus) added dismissable / hoverable / persistent requirements that sharply raised the cost of an APG-style tooltip. The W3C ARIA WG itself has acknowledged in issues and TPAC 2023 discussions that the current tooltip role cannot express rich-content use cases and needs rework. Toggletips draw their own criticism — essential information should not hide behind a tiny button — so they are a leading alternative, not a settled consensus.

Adopted by:Sarah HigleyHeydon Pickering (Inclusive Components)

Sources:Sarah Higley — Tooltips in the time of WCAG 2.1(opens in a new tab)Inclusive Components — Tooltips & Toggletips(opens in a new tab)w3c/aria-practices#128 — Tooltip pattern consensus(opens in a new tab)