( Operable / WCAG 2.4.3 )
Modal is missing focus management
What is this issue?
A modal or overlay container (identified by its id or class name matching common naming patterns like “modal,” “overlay,” “drawer,” “panel,” or “flyout”) is visible on screen (rendered, not hidden or inert) but has no tabindex attribute anywhere in its markup. Non-interactive elements like <div> and <section> aren’t focusable by default; calling .focus() on one with no tabindex attribute silently does nothing.
This is a narrower, structural check than whether a modal correctly manages focus overall. It only asks whether the container itself is even capable of receiving programmatic focus: the basic plumbing a correctly built modal needs before any open-dialog focus-movement code can succeed. A container missing this plumbing makes correct focus management impossible from the start, regardless of how well-intentioned the rest of the modal’s code is.
Why does this matter?
Focus management for a dialog almost always depends on being able to call .focus() on the container itself, particularly when the dialog has no natural first focusable element to target instead: a confirmation dialog with just a heading and two buttons, for instance, where focusing the heading directly (with its own tabindex="-1") is a common pattern that only works if the surrounding container’s own focus plumbing is also correctly understood by whoever built it.
Missing tabindex on the container is also a strong leading indicator that focus management was never implemented at all, rather than implemented incorrectly. Teams that have thought about keyboard focus for a modal tend to add tabindex="-1" to the container as a matter of course, so its total absence usually signals the modal was built with only mouse interaction in mind.
Who is affected?
- Keyboard users: a modal missing this basic focusability plumbing has no reliable way to receive keyboard focus when it opens, leaving them stranded on the page behind it with no path forward via the keyboard alone.
- Screen reader users: depend on focus movement into the dialog to trigger an announcement of its content; a container that structurally cannot receive focus blocks that announcement from ever happening, regardless of what ARIA roles are otherwise present.
What users experience
Felix uses keyboard-only navigation due to a motor impairment affecting fine mouse control. A settings page’s “Change password” link opens a modal built as a plain <div class="modal"> with no tabindex attribute anywhere, and the developer’s open-dialog function calls modal.focus() expecting it to work like a native form control. The call silently does nothing, because the element has no tabindex to make it focusable. Felix’s keyboard focus stays on the “Change password” link he just activated; the modal is visible on screen, but Tab moves him to the next link in the page’s main navigation instead of into the password fields now covering it, and he has no way to reach the form that just appeared.
How do I fix it?
Add tabindex="-1" to the modal container. This works because it makes the container programmatically focusable (a target .focus() calls can actually succeed against) while keeping it out of the page’s normal Tab sequence, since -1 specifically means “focusable via script, not via Tab,” which is exactly the behavior a dialog container needs.
This fix addresses the structural precondition only; pair it with the behavioral fix described in Modal opens without moving focus into it: actually calling .focus() on the container (or an element inside it) when the dialog opens, and returning focus to the trigger element when it closes, to get a fully working modal focus experience rather than just a focusable-but-unused container.
Code Examples
<div class="modal" role="dialog" aria-modal="true">
<h2>Change your password</h2>
<!-- form fields -->
</div>modal.style.display = 'block';
modal.focus(); // silently does nothing - no tabindex present<div class="modal" role="dialog" aria-modal="true" tabindex="-1">
<h2>Change your password</h2>
<!-- form fields -->
</div>modal.style.display = 'block';
modal.focus(); // now succeeds - the container is programmatically focusableAdding tabindex="-1" is the entire structural fix. Nothing else about the markup changes. The same .focus() call that previously did nothing now works, because the container finally meets the one precondition every non-natively-interactive element needs before JavaScript can move focus to it.
Common Mistakes
Mistake: “The modal has role=‘dialog’, so it must already be focusable; ARIA roles imply focus behavior.” ARIA roles describe what an element is to assistive technology; they carry no browser focus behavior on their own. role="dialog" with no tabindex is exactly as unfocusable via script as a plain <div> with no role at all: the role and the focusability are entirely independent attributes.
Mistake: “We use a UI library, so focus management for modals is handled for us.” Many popular component libraries do handle this correctly, but not universally, and a customized or older version can easily be missing it. Confirm your specific library’s modal component actually sets tabindex on its container rather than assuming any library automatically covers this.
Mistake: “Adding tabindex=‘-1’ to the container is the whole fix for accessible modal focus.” It’s the necessary first step, not the whole fix. The container being capable of receiving focus doesn’t mean anything actually moves focus there when the dialog opens: that behavioral piece is a separate requirement, covered by a different RedFlag check, that still needs its own implementation and testing.
How RedFlag Detects This
Custom automated: RedFlag’s own DOM detector, runs on every scan. RedFlag selects div and section elements whose id or class name matches a pattern for modal, overlay, drawer, panel, or flyout components, then flags any that are visibly rendered (a non-zero bounding rect, not aria-hidden, not inert, not hidden via display: none or visibility: hidden) and have no tabindex attribute present at all.
False negative: a modal container using naming conventions the detector’s pattern doesn’t match (a custom class name with no “modal,” “overlay,” “drawer,” “panel,” or “flyout” substring) is invisible to this check entirely, regardless of whether it has the same missing-tabindex problem. False positive: an element whose name happens to match the pattern (a “sidebar-panel” used for entirely non-modal navigation, for instance) can be flagged even though it was never meant to receive programmatic focus at all. Manual step: for any flagged element, confirm it’s genuinely a modal or overlay before adding tabindex, and separately verify actual open/close focus behavior, which this structural check doesn’t test.
Manual Testing
- Open the page’s HTML source or DevTools’ Elements panel and locate every modal, overlay, or drawer container.
- Check each one for a
tabindexattribute: its presence (commonlytabindex="-1") confirms the structural precondition this rule tests. - Trigger the modal and, in the browser console, confirm calling
.focus()on the container actually moves focus there (DevTools’ Elements panel highlights the currently focused element). - If focus doesn’t move when
.focus()is called, addtabindex="-1"to the container and repeat the test. - Follow up with the full behavioral test in Modal opens without moving focus into it to confirm focus actually moves automatically on open, not just that it’s technically possible.
Related WCAG Success Criteria
2.4.3 Focus Order: Components must receive focus in an order that preserves meaning and operability. A modal container that structurally cannot receive focus makes correct focus order impossible for that dialog by definition, since there’s no valid target for focus to move to when the dialog opens.
Related Issues
Modal opens without moving focus into it is this rule’s behavioral companion: the AI-assisted check for whether focus actually moves into a modal on open and returns to the trigger on close, which depends on this rule’s structural fix being in place first.
Focus is trapped inside a component covers the next stage of the same dialog lifecycle, once focus correctly enters a modal: whether it can also get back out again.
Manual check flagged an incorrect tab order and Element has a positive tabindex cover related focus-sequencing defects in the same 2.4.3 Focus Order family, applied to static page content rather than dialog structure.
References
Frequently asked questions
How is this different from Modal opens without moving focus into it?
This rule checks the structural precondition: does the modal container even have a tabindex attribute, which is required before JavaScript can call .focus() on it successfully. Modal opens without moving focus into it (redflag-modal-focus) checks the behavior built on top of that precondition: does the open-dialog code actually call .focus() at the right moment, and does focus return to the trigger on close.
Why does an element need tabindex to be focused programmatically?
Elements are only focusable via JavaScript's .focus() method if they are natively interactive (like a button or input) or explicitly given a tabindex attribute. A plain div or section, no matter how it is styled or what role it has, silently ignores .focus() calls unless tabindex is present, even tabindex="-1", which keeps it out of the normal Tab sequence while still allowing this exact programmatic use.
Should the modal container use tabindex="0" or tabindex="-1"?
Almost always tabindex="-1". That value makes the container programmatically focusable without adding it to the page's regular Tab sequence, which is what you want: the container itself is not meant to be a Tab stop a keyboard user lands on repeatedly, only a target focus can be moved to once, when the dialog opens.
Does giving the container tabindex="-1" alone make the modal fully accessible?
No, it only satisfies this rule's structural check: it makes the container capable of receiving focus, nothing more. Actually moving focus into it when the dialog opens, and returning focus to the trigger element on close, is separate behavior this rule does not test, covered instead by Modal opens without moving focus into it.