How to Fix Accessibility Issues in Modals and Popups
Modals and popups are some of the easiest components to get wrong. They appear simple visually: a box opens on top of the page, the background dims, and the user chooses an action. But accessibility depends on much more than the visual overlay.
A modal needs to manage focus. It needs a clear name. It needs keyboard support. It needs a close mechanism. It needs to prevent users from accidentally interacting with the background page. It should announce itself as a dialog to assistive technology. It should return users to a logical place when it closes. If any of those pieces are missing, the modal can become confusing or unusable.
Common modal problems include focus staying behind the modal, keyboard users tabbing into the hidden background, no visible close button, close buttons without accessible names, Esc not working, screen readers not knowing a dialog opened, and focus disappearing after the modal closes.
These issues are especially serious when the modal contains a login form, checkout step, newsletter signup, cookie banner, support chat, appointment scheduler, or accessibility scan form. If users cannot operate the modal, they may not be able to complete the task. ADA CodeFix flags modal and popup issues because they often block important user flows. This guide explains how to build accessible modals with proper semantics, focus management, keyboard behavior, labels, and testing steps.
This page is informational and is not legal advice. Accessibility requirements can vary by site, organization, and jurisdiction. For legal guidance, consult qualified counsel. For accessibility conformance, combine automated scans with manual testing.
What makes a modal accessible?
An accessible modal should:
- Be announced as a dialog
- Have a clear accessible name
- Move focus into the modal when opened
- Keep focus inside while open
- Provide a visible and accessible close button
- Usually close with the
Esckey - Prevent background content from being reached while open
- Return focus to the triggering element when closed
- Maintain a logical reading and focus order
- Avoid trapping users permanently
A modal is not only a visual layer. It is a temporary interaction mode. The page essentially pauses while the modal is open, and the user's focus, keyboard, and assistive technology all need to follow that change.
WCAG criteria related to modals
WCAG 2.1.2 — No Keyboard Trap
Users should not get stuck in a component. A modal may intentionally keep focus inside while open, but users must have a clear way to close it or move forward. The difference between an intentional focus loop and a keyboard trap is whether the user has a known way out.
WCAG 2.4.3 — Focus Order
Focus should move in a logical order. When a modal opens, focus should move into the modal. When it closes, focus should return to a logical place — typically the element that opened the modal. Focus that jumps to the top of the page or disappears entirely fails this criterion.
WCAG 2.4.7 — Focus Visible
Keyboard users must be able to see which control inside the modal has focus. Removing focus outlines inside dialogs is a common mistake. Even if the modal is short, a visible focus indicator on every interactive element is required.
WCAG 4.1.2 — Name, Role, Value
A modal dialog should expose the correct role, name, state, and relationships to assistive technology. A generic div that visually looks like a dialog but lacks the proper role and label can be invisible to screen readers as a discrete component.
Basic accessible modal structure
A common ARIA modal pattern looks like this:
<button type="button" id="open-scan-dialog">
Start free scan
</button>
<div
id="scan-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="scan-dialog-title"
aria-describedby="scan-dialog-description"
hidden
>
<div class="dialog-panel">
<h2 id="scan-dialog-title">Start a free accessibility scan</h2>
<p id="scan-dialog-description">
Enter a public page URL to check for common accessibility issues.
</p>
<label for="scan-url">Page URL</label>
<input id="scan-url" type="url" placeholder="https://example.com">
<button type="button" id="close-scan-dialog">
Cancel
</button>
<button type="submit">
Scan page
</button>
</div>
</div>This structure provides a button that opens the modal, a dialog role, aria-modal="true", a visible title connected with aria-labelledby, a description connected with aria-describedby, a visible close action, and form controls with labels. The JavaScript behavior matters just as much as the markup.
Opening the modal
When the modal opens, store the element that triggered it, show the modal, and move focus inside.
const openButton = document.getElementById("open-scan-dialog");
const dialog = document.getElementById("scan-dialog");
const closeButton = document.getElementById("close-scan-dialog");
let previouslyFocusedElement = null;
openButton.addEventListener("click", () => {
previouslyFocusedElement = document.activeElement;
dialog.hidden = false;
closeButton.focus();
});Depending on the modal, focus may move to the dialog container, heading, first field, close button, or primary action. For a form modal, focusing the first field may be helpful. For a confirmation dialog, focusing the heading or close button may be better. If you focus the heading, add tabindex="-1":
<h2 id="scan-dialog-title" tabindex="-1">
Start a free accessibility scan
</h2>document.getElementById("scan-dialog-title").focus();The tabindex="-1" makes the heading programmatically focusable without inserting it into the natural tab order. The user can move focus there with JavaScript, then tab forward to interactive controls inside the dialog.
Closing the modal
When the modal closes, hide it and return focus to the element that opened it.
function closeDialog() {
dialog.hidden = true;
if (previouslyFocusedElement) {
previouslyFocusedElement.focus();
}
}
closeButton.addEventListener("click", closeDialog);Returning focus matters. If focus disappears or jumps to the top of the page, keyboard users may lose their place and have to tab back through every link and control to reach the same area again. Always store the trigger and restore focus when the dialog closes.
Escape key support
Many users expect Esc to close a modal. This is a well-established convention and is the typical behavior on desktop operating systems.
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !dialog.hidden) {
closeDialog();
}
});There are exceptions. If closing the modal would lose user data, you may need confirmation. But for most popups and dialogs, Esc support is expected, and removing it for stylistic reasons is a real barrier for keyboard users.
Keeping focus inside the modal
While a modal is open, keyboard focus should not move to the background page. A simplified focus trap:
function getFocusableElements(container) {
return container.querySelectorAll(
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
);
}
dialog.addEventListener("keydown", (event) => {
if (event.key !== "Tab") return;
const focusable = Array.from(getFocusableElements(dialog));
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
}
if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});For production, consider using a well-tested accessible dialog component or library. Focus management has many edge cases, including elements that become focusable mid-interaction, nested dialogs, and content inserted into the dialog dynamically. A mature dialog library handles these for you.
Preventing background interaction
A modal should make the background page inactive while open. Options include the native <dialog> element, the inert attribute, focus trapping, hiding the modal content when closed, and preventing pointer interaction with the background.
Example with inert:
<main id="page-content">
...
</main>
<div id="scan-dialog" role="dialog" aria-modal="true" hidden>
...
</div>const pageContent = document.getElementById("page-content");
function openDialog() {
previouslyFocusedElement = document.activeElement;
dialog.hidden = false;
pageContent.inert = true;
closeButton.focus();
}
function closeDialog() {
dialog.hidden = true;
pageContent.inert = false;
previouslyFocusedElement?.focus();
}The inert attribute removes its subtree from the tab order and from assistive technology while the modal is open. Check browser support and test carefully — both pointer events and screen reader behavior should be verified before relying on inert in production.
Native HTML dialog
The native <dialog> element can help with modal behavior, including a built-in close mechanism, automatic focus handling, and background inertness.
<dialog id="scan-dialog" aria-labelledby="scan-dialog-title">
<h2 id="scan-dialog-title">Start a free accessibility scan</h2>
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="confirm">Start scan</button>
</form>
</dialog>const dialog = document.getElementById("scan-dialog");
dialog.showModal();Native dialog support has improved, but you should still test keyboard behavior, focus return, labels, and styling across your supported browsers and assistive technology combinations. The native element is a strong starting point, but it is not a replacement for testing the full open-to-close flow.
Close buttons
Every modal should have a visible way to close it unless the modal is a required step in a controlled process. Icon-only close buttons need accessible names.
<button type="button" aria-label="Close dialog">
<svg aria-hidden="true" focusable="false">...</svg>
</button>A visible text button is also good:
<button type="button">
Close
</button>Avoid tiny close targets. Make the button large enough to operate comfortably on touch screens and for users with motor disabilities. A common minimum is 44 by 44 CSS pixels for the interactive hit area.
Modal titles and descriptions
A dialog should have a clear accessible name. The most common pattern is aria-labelledby, which points at the visible heading inside the modal.
<div
role="dialog"
aria-modal="true"
aria-labelledby="delete-title"
aria-describedby="delete-description"
>
<h2 id="delete-title">Delete saved scan?</h2>
<p id="delete-description">
This action cannot be undone.
</p>
</div>Do not rely only on visual styling. Connect the title programmatically so screen readers announce the dialog with its purpose when it opens. Use aria-describedby for supporting text that explains the consequences or instructions.
Common modal accessibility failures
Failure 1: Focus stays behind the modal
The modal opens visually, but keyboard focus remains on the button behind it. A keyboard user presses Tab and continues through the background page. Fix this by moving focus into the modal when it opens.
Failure 2: Background is still reachable
The modal is open, but users can tab to header links, footer links, or form fields behind the overlay. Fix this by trapping focus inside the modal and making background content inert or otherwise unavailable.
Failure 3: No accessible name
Problem:
<div role="dialog" aria-modal="true">
<h2>Subscribe to updates</h2>
</div>Better:
<div
role="dialog"
aria-modal="true"
aria-labelledby="subscribe-title"
>
<h2 id="subscribe-title">Subscribe to updates</h2>
</div>Failure 4: Close button has no name
Problem:
<button>
<svg aria-hidden="true">...</svg>
</button>Better:
<button aria-label="Close newsletter signup">
<svg aria-hidden="true" focusable="false">...</svg>
</button>Failure 5: Focus does not return after close
If focus jumps to the top of the page, users may have to navigate back to where they were. Store the opener and return focus after closing.
How ADA CodeFix detects modal issues
ADA CodeFix can flag common modal and popup problems such as:
- Dialogs without accessible names
- Missing
aria-modal - Close buttons without names
- Focusable background content when modal appears open
- Hidden modals with focusable elements
- Missing focus indicators inside dialogs
- Buttons that open dialogs without state or focus handling
- Popups that may create keyboard traps
- Modal markup that uses generic
divs without dialog semantics
Automated tools can identify many markup problems. They cannot fully verify the focus sequence, keyboard trap behavior, or whether focus returns correctly. Manual testing is required before treating a modal as accessible.
Manual modal testing checklist
Use this checklist after making changes.
- Can the modal be opened with the keyboard?
- When it opens, does focus move into the modal?
- Does the modal have a clear title?
- Is the title connected with
aria-labelledby? - Is helpful description text connected with
aria-describedbywhen appropriate? - Can the modal be closed with a visible button?
- Does the close button have an accessible name?
- Does
Escclose the modal when appropriate? - Does Tab stay inside the modal while it is open?
- Does Shift+Tab stay inside the modal while it is open?
- Is background content unreachable while the modal is open?
- Is focus visible on every modal control?
- After closing, does focus return to the opener?
- Can the user complete the modal task using only the keyboard?
- Does the modal work at mobile and desktop sizes?
A screen reader pass is also important. Listen for the dialog announcement when it opens, confirm the accessible name is read, and verify that the close button is labeled before activating it.
Platform-specific notes
WordPress
Popup plugins, newsletter modals, cookie banners, and page-builder dialogs often need accessibility testing. Do not assume a plugin is accessible because it is popular. Check focus handling, escape support, and whether the background remains tabbable when the popup is shown.
Shopify
Review cart drawers, newsletter popups, size guides, quick-view product modals, discount popups, and search overlays. Cart drawers are especially important because they affect checkout. A cart drawer that traps focus poorly or hides its close button on small screens can block purchases.
Webflow
Interactions can open modal-like overlays, but focus management may need custom code. Test whether background links remain focusable, whether Esc works, and whether focus returns to the trigger.
React and Next.js
Use a tested dialog component when possible. If building your own, manage focus, escape key behavior, background inertness, labels, and focus return. A minimal React pattern should store the opener and restore focus:
function Modal({ open, onClose, titleId, children }) {
if (!open) return null;
return (
<div role="dialog" aria-modal="true" aria-labelledby={titleId}>
{children}
<button type="button" onClick={onClose}>
Close
</button>
</div>
);
}This markup is only the start. Production modals need focus management, escape handling, scroll lock on the body, and a way to return focus to the trigger. Libraries that wrap these behaviors are usually worth the dependency.
Final takeaway
Modals are accessibility-sensitive because they temporarily change how the page works. A visual overlay is not enough. Users need focus to move into the modal, stay inside while it is open, and return to a logical place when it closes. The background page should be unreachable, the dialog should announce itself, and the close mechanism should be obvious and accessible.
The safest approach is to use a proven accessible dialog pattern, give the modal a clear name, manage focus intentionally, provide a visible close button, support keyboard operation, and test the full open-to-close flow without a mouse. ADA CodeFix can help identify likely modal issues, but manual testing is essential before treating a modal as accessible.
Sources
- W3C WAI-ARIA Authoring Practices — Dialog Pattern (opens in new tab)
- W3C WCAG Understanding 2.1.2 No Keyboard Trap (opens in new tab)
- W3C WCAG Understanding 2.4.3 Focus Order (opens in new tab)
- W3C WCAG Understanding 4.1.2 Name, Role, Value (opens in new tab)
- W3C Web Accessibility Initiative (WAI) (opens in new tab)
- MDN — HTML dialog element (opens in new tab)
Run a free WCAG 2.1 AA scan on your site
ADA CodeFix scans your pages, identifies likely WCAG failures, and generates developer-reviewable code fixes for dialogs, focus management, labels, alt text, color contrast, and more.
Scan My Site FreeRelated guides
Fix clickable divs, hover-only menus, and focus traps that break modal dialogs.
Focus-visible stylesVisible focus indicators on every control inside the dialog.
ARIA labels done rightUse aria-labelledby and aria-describedby to give dialogs clear names.
WCAG 2.1.2 No Keyboard TrapWhy focus loops inside dialogs must always have an exit.