DEV Community

A11ySolutions
A11ySolutions

Posted on

The One Accessibility Bug That Shows Up in Every Audit, No Matter the Industry

Open a modal. A cart drawer, a signup form, a date picker, a payment confirmation. Now ask three questions: did focus move into it, can a keyboard user get trapped inside it or escape it cleanly, and when it closes, does focus go back to whatever triggered it? Across every audit in our issue bank, cutting across hospitality, banking, retail, healthcare, streaming, and media, the answer to at least one of those questions is no. It is the single most repeated pattern we've documented, and it has nothing to do with industry.

Why this one keeps winning

Most accessibility bugs are specific to a component: a carousel missing aria-roledescription, a contrast ratio that misses 4.5:1. Focus management in dialogs fails for a more structural reason: it requires three separate things to happen correctly (move focus in, contain it, return it on close), and most implementations only handle the part that's visually obvious, showing and hiding the dialog, while treating focus as an afterthought, if they treat it at all.

That's why the same defect shows up in a checkout modal on a retail site, a "subscribe" popup on a media site, and a date picker on a banking form. Different teams, different stacks, same structural gap.

What it actually looks like

  • Focus doesn't move on open. The dialog appears visually, but keyboard focus stays wherever it was on the page behind it. A screen reader user has no idea anything happened. An AI agent reading the accessibility tree sees a new dialog node exist, but has no signal telling it that's where interaction should now happen.

  • Focus isn't trapped. Tabbing from the last interactive element inside the dialog moves focus back onto the page behind it, while the dialog is still visually open and, in many implementations, still blocking the rest of the page from receiving clicks. The user is now interacting with a page they can't see or reach.

  • Focus doesn't return on close. The dialog closes, and focus resets to the top of the document instead of returning to the button or link that opened it, or is simply dropped and lost. Every dismissal costs the user their place in the page.

This is a real, anonymized example from one of our audits, a "view more info" dialog with the ARIA already in place:

html
<button class="modal-trigger">See more details</button>
...
<section class="modal-content">
  <button type="button" class="modal-close" aria-label="Close dialog">
    <span aria-hidden="true">...</span>
  </button>
  <section class="modal-wrapper">
    <!-- dialog content -->
  </section>
</section>
Enter fullscreen mode Exit fullscreen mode

Nothing here is wrong at the markup level. aria-label is present on the close button, the structure looks reasonable. But there is no role="dialog", no aria-modal, and critically, nothing in the code moves focus anywhere when this section becomes visible. That's part of why this pattern survives so many audits before someone catches it manually: it looks fine in the DOM inspector, and automated scanners have no way to test what focus does at runtime.

Across every real case we've documented, the fix the team lands on is the same, regardless of framework or client: move focus into the dialog on open, trap it inside the dialog's boundaries while it's open, and return it to the element that opened the dialog on close, implemented through JavaScript functions following the WAI-ARIA Authoring Practices dialog pattern. That's not a stylistic preference, it's the one approach that consistently closes all three gaps at once, which is why it shows up as the recommendation on every audit we've run, on every dialog, in every client, independent of what UI library built the component in the first place.

Why an AI agent hits the exact same wall

An agent operating a browser doesn't get a special channel into your component state. It perceives the page through the accessibility tree, the same structure a screen reader consumes. When a dialog opens without a focus shift, the agent has no reliable signal that a new interactive context now exists and needs attention. When focus isn't trapped, an agent attempting to fill a field or press a button inside the dialog can just as easily land on an element behind it, because nothing in the tree communicated a change of scope. When focus doesn't return on close, the agent's next action executes against whatever element happens to be focused, not necessarily the one it intended to return to.

The dialog can be visually perfect and functionally invisible to anything, human or agent, that navigates by focus instead of by sight.

Why it's worth fixing first

Of every pattern in our audit data, this is the one with the widest blast radius relative to effort. It isn't industry-specific, it isn't component-specific within a given codebase (the same dialog component usually gets reused everywhere), and the fix is a contained, well-documented pattern rather than a redesign. If a team fixes exactly one thing this quarter, the dialog focus lifecycle is the one that shows up in the most places for the least rework, and it's the one where our own audit data gives the clearest, most consistent answer for what "fixed" looks like.

Top comments (0)