You do not need to uninstall your popup apps to fix their accessibility problems. Most of the failures follow the same four patterns, and all four can be patched at the storefront level with a small amount of JavaScript and HTML. This guide walks through exactly what breaks in newsletter modals, cookie banners, discount wheels, and age gates, which WCAG criteria they violate, and the specific fixes you can apply without touching the app itself.
If you want a broader view of how third-party apps introduce accessibility issues across Shopify stores, the Shopify third-party app WCAG violations guide covers the full pattern. This post zooms in on popup-type components specifically.
Why popup apps break accessibility so reliably
Popup apps work by injecting a modal overlay into the DOM after the page has loaded. The app script creates a container, drops it in the <body>, and makes it visible. The problem is that almost none of these apps follow the WAI-ARIA dialog modal authoring pattern when they do this.
The result is a modal that looks fine with a mouse but may be unnamed to a screen reader, unreachable by keyboard, or impossible to dismiss without clicking.
Four failures show up again and again in popup app audits:
- The container has no
role="dialog", so screen readers do not announce it as a dialog. - Focus does not move into the modal when it opens. A keyboard user is still tabbing through the page behind it.
- There is no Escape key handler. The only close path is clicking the X button.
- The X button is an icon with no accessible name, so even if a keyboard user finds it, their screen reader says “button” with no context.
That list alone covers WCAG 2.1.1 (Keyboard), 2.4.3 (Focus Order), and 4.1.2 (Name, Role, Value), all of which are Level A or AA. These are not edge cases. They are the baseline.
The WCAG criteria that actually apply
The six criteria below matter most for popup apps:
WCAG 2.1.1 Keyboard (Level A): Every action in the modal must be completable with a keyboard. That includes closing it, submitting a form, and navigating between fields.
WCAG 2.4.3 Focus Order (Level A): When the modal opens, focus must move into it. When the modal closes, focus must return to the element that triggered it. Modals that steal neither focus nor keyboard attention violate this criterion because the effective focus order breaks down.
WCAG 4.1.2 Name, Role, Value (Level A): Every UI component must have an accessible name, an appropriate role, and expose its state. A <div> that acts as a button has no role. An icon-only close button has no name. A modal container with no role="dialog" is reported as nothing by screen readers.
WCAG 1.3.1 Info and Relationships (Level A): Form fields in the modal, like the email field in a newsletter popup, must be labeled. A placeholder attribute is not a label. When the placeholder disappears on focus, screen reader users have no way to know what they are filling in.
WCAG 1.4.3 Contrast Minimum (Level AA): Discount wheels, countdown banners, and cookie notices often use low-contrast text over background images or tinted overlays. The minimum ratio is 4.5:1 for normal text and 3:1 for large text. Most popup app default themes fail this.
WCAG 2.1.2 No Keyboard Trap (Level A): This one works in both directions for a modal. If focus enters the modal, it must stay there while the modal is open, and the user must be able to exit by pressing Escape or reaching a close control. If focus can leak out to the body behind the modal, keyboard and screen reader users face a broken context. If focus enters but cannot escape at all, the user is trapped.
QA checklist for popup app accessibility
Run this with a keyboard only, with your mouse unplugged or set aside. Then run it again with VoiceOver on macOS or NVDA on Windows.
- Load the page and wait for the popup to appear without interacting with it. Note whether the screen reader announces anything.
- Press Tab. Note whether focus is now inside the modal or still cycling through the page behind it.
- Inside the modal, press Tab repeatedly. Confirm focus stays within the modal and does not escape to the page.
- Press Shift+Tab. Confirm you can cycle backwards within the modal.
- Press Escape. Confirm the modal closes.
- After the modal closes, confirm focus has returned to the page element that originally had focus, or to a sensible fallback like the page header.
- If the modal has a form, navigate to each field. Confirm the screen reader reads a label for each one, not just a placeholder.
- If the modal has a close button with an icon only, confirm the screen reader reads a meaningful label, not just “button.”
- Open the modal again and check contrast with a tool like the browser’s DevTools accessibility panel or the Shopify Accessibility Fixer checker. Flag any text below 4.5:1.
- Tab to the submit or continue button. Confirm the button has a name that describes what it does, like “Subscribe to newsletter” rather than just “Submit.”
Log each step as pass, fail, or partial. A partial means the behavior sort of works but with degraded experience.
The popup container: before and after
Most popup apps render something like this:
<!-- BEFORE: common app output -->
<div class="popup-wrapper popup-active">
<div class="popup-overlay"></div>
<div class="popup-box">
<div class="popup-close">
<svg viewBox="0 0 24 24">...</svg>
</div>
<h2>Get 10% off your first order</h2>
<input type="text" placeholder="Your email address" />
<button>Subscribe</button>
</div>
</div>
Screen readers see: a generic container, some images, some text, an anonymous button. Nothing announces itself as a dialog. Focus stays where it was before the popup appeared.
The patched version looks like this:
<!-- AFTER: patched at the storefront level -->
<div
class="popup-wrapper popup-active"
role="dialog"
aria-modal="true"
aria-labelledby="popup-heading"
tabindex="-1"
>
<div class="popup-overlay" aria-hidden="true"></div>
<div class="popup-box">
<button
class="popup-close"
type="button"
aria-label="Close offer popup"
>
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24">...</svg>
</button>
<h2 id="popup-heading">Get 10% off your first order</h2>
<label for="popup-email" class="visually-hidden">Email address</label>
<input id="popup-email" type="email" autocomplete="email" />
<button type="submit">Subscribe to newsletter</button>
</div>
</div>
Changes:
role="dialog"andaria-modal="true"on the wrapperaria-labelledbypointing to the heading, so screen readers announce “dialog, Get 10% off your first order”tabindex="-1"on the wrapper so JavaScript can move focus to it programmaticallyaria-hidden="true"on the decorative overlay- Close button has an explicit
aria-labelinstead of relying on the SVG - Email field has a real
<label>, not just a placeholder - Submit button has a meaningful label
You cannot change the app’s HTML directly. But you can patch it at runtime from a Shopify theme snippet or a custom script block.
JavaScript to patch any popup at runtime
This script runs after the popup appears and applies the missing attributes. Add it to your theme’s layout/theme.liquid or as a Shopify app embed block:
(function patchPopupAccessibility() {
function patch(popup) {
if (popup.dataset.a11yPatched) return;
popup.dataset.a11yPatched = 'true';
// Add dialog role and label
if (!popup.getAttribute('role')) {
popup.setAttribute('role', 'dialog');
}
popup.setAttribute('aria-modal', 'true');
if (!popup.getAttribute('tabindex')) {
popup.setAttribute('tabindex', '-1');
}
// Find the heading and use it as label
const heading = popup.querySelector('h1, h2, h3, h4');
if (heading) {
if (!heading.id) heading.id = 'popup-heading-' + Date.now();
popup.setAttribute('aria-labelledby', heading.id);
}
// Label the close button
const closeBtn = popup.querySelector('[class*="close"], [class*="dismiss"]');
if (closeBtn && !closeBtn.getAttribute('aria-label')) {
closeBtn.setAttribute('aria-label', 'Close');
}
// Hide decorative overlays
const overlay = popup.querySelector('[class*="overlay"], [class*="backdrop"]');
if (overlay) overlay.setAttribute('aria-hidden', 'true');
// Move focus in
const firstFocusable = popup.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
setTimeout(function() {
(firstFocusable || popup).focus();
}, 50);
}
// Watch for popup apps injecting content
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType !== 1) return;
const popup = node.querySelector('[class*="popup"], [class*="modal"]') || node;
if (
popup.classList.toString().match(/popup|modal|overlay|lightbox/i) &&
getComputedStyle(popup).display !== 'none'
) {
patch(popup);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
This is a starting point, not a complete solution. It handles the most common gaps, but some apps use shadow DOM or custom rendering that requires a more targeted fix. For those, contact the app vendor or use a tool that handles them automatically.
App-specific failure patterns
Newsletter modals
Newsletter popups almost always miss the <label> on the email field. The placeholder text disappears on focus, leaving keyboard and screen reader users without a hint. The fix is a visually hidden label associated with the input by for/id.
The subscribe button is commonly labeled “Submit” or left with no text at all, just an arrow icon. Change the accessible name to “Subscribe” or “Get my discount,” whichever matches the intent.
Cookie banners
Cookie banners are often the most urgent case because they block the entire page and appear on every first visit. If the banner cannot be dismissed by keyboard, users who rely on keyboard navigation are locked out of the site until they interact with a mouse.
Cookie banners also frequently use small gray text on white backgrounds for the “reject” or “manage settings” link, failing WCAG 1.4.3. The reject path should have the same visual weight as the accept path.
One specific pattern worth checking: some cookie apps render the “Decline” button as a plain link styled like a button. If the link has no href, it is not keyboard-focusable by default. Add tabindex="0" and a keydown handler for Enter, or change the element to a real <button>.
Discount wheels
Discount wheels, also known as spin-to-win apps, are one of the hardest popup types to remediate. The canvas or SVG animation is inaccessible by design. You cannot make a spinning wheel accessible to screen readers without a completely different UX for non-visual users.
The practical fix is a two-step approach. First, give the trigger button a clear accessible name: “Open discount wheel.” Second, when the wheel stops, announce the result in a live region:
<div aria-live="polite" aria-atomic="true" class="visually-hidden" id="wheel-result"></div>
function announceWheelResult(prizeLabel) {
var region = document.getElementById('wheel-result');
if (!region) return;
region.textContent = '';
requestAnimationFrame(function() {
region.textContent = 'You won ' + prizeLabel + '. Use code shown to claim your discount.';
});
}
The wheel animation itself can stay as-is. The announcement gives screen reader users the same key information.
Age gates
Age gates that use dropdown selects or date inputs are usually keyboard-accessible by default. The common failure is an age gate that uses styled <div> elements to simulate a date picker, with no keyboard support and no form association.
A compliant age gate uses real <select> elements with associated labels:
<fieldset>
<legend>Enter your date of birth to confirm your age</legend>
<label for="age-month">Month</label>
<select id="age-month" name="month" autocomplete="bday-month">
<option value="">Month</option>
...
</select>
<label for="age-year">Year</label>
<select id="age-year" name="year" autocomplete="bday-year">
<option value="">Year</option>
...
</select>
</fieldset>
<button type="submit">Confirm age</button>
A <fieldset> with a <legend> groups the date fields into a single named group. This satisfies WCAG 1.3.1 and gives context to screen reader users who would otherwise hear “Month, select” with no surrounding explanation.
Evidence log for popup remediations
When you fix an issue, record it. If your store ever receives a demand letter or an audit request, this log shows that you identified the problem and acted on it.
| App name | Issue found | WCAG criterion | Severity | Fix applied | Retest date | Result |
|---|---|---|---|---|---|---|
| Newsletter popup | No role=“dialog”, focus not moved in | 4.1.2, 2.4.3 | Blocker | Runtime patch via theme.liquid | 2026-06-03 | Pass |
| Cookie banner | Decline button not keyboard-focusable | 2.1.1 | Blocker | Added tabindex and keydown handler | 2026-06-03 | Pass |
| Discount wheel | No live region announcement | 4.1.3 | Major | Added aria-live region with prize text | 2026-06-03 | Pass |
| Age gate | Custom date dropdowns, no labels | 1.3.1, 4.1.2 | Major | Vendor contacted, pending update | --- | Open |
| Cookie banner | Low contrast on “Decline” link (2.8:1) | 1.4.3 | Major | Color updated via theme CSS override | 2026-06-03 | Pass |
Record the screen reader and version you used for each retest. VoiceOver on Safari and NVDA on Chrome can behave differently, and documenting both matters.
How AmazingPlugins helps
The Shopify Accessibility Fixer app automates several of the fixes described above. It scans for popup containers that lack dialog semantics and patches them at runtime. It detects unlabeled close buttons and adds accessible names. It catches focus management failures and applies the focus-on-open and return-on-close logic without requiring changes to any app or theme file.
The app also runs a page-level accessibility checker that flags which popup apps are active and which specific issues they are introducing. This is useful if you have five or six apps and are not sure which one is responsible for a given failure.
For a broader comparison of what different Shopify accessibility tools actually do differently, the Shopify accessibility app comparison guide walks through the trade-offs between overlays, checkers, and code-level fixes. The short version: runtime patches are faster to ship but require ongoing maintenance, and a code-level fix is more stable if the app vendor supports it.
If you want to fix things without the app, the patterns in this post are stable. They follow the W3C WCAG 2.2 specification and the WAI-ARIA dialog modal pattern, and those standards do not change frequently.
For stores with multiple apps causing overlapping failures, the guide to fixing Shopify app accessibility errors without uninstalling covers the broader app ecosystem.
FAQ
Can I fix popup app accessibility without access to the app’s source code?
Yes. The runtime patching approach works by observing DOM mutations and adding missing attributes after the app injects its content. You do not need to modify the app files at all. The limitation is that shadow DOM components are harder to reach, and some highly animated apps require more targeted code. Start with the MutationObserver pattern above and escalate to app-specific fixes as needed.
Does adding aria-modal="true" create a focus trap automatically?
No, and this is a common misconception. aria-modal="true" is a hint to assistive technology, not a browser enforcement mechanism. You still need to write the JavaScript focus trap yourself. See the focus trap pattern in the Shopify cart drawer accessibility fixes guide for a reliable implementation you can adapt to any modal.
My popup app vendor says their app is WCAG compliant. Should I trust that?
Test it yourself before accepting the claim. Run the QA checklist above. Pay specific attention to keyboard-only navigation and screen reader announcements. A lot of apps pass automated scanners but fail manual testing because automated tools cannot detect missing focus management or incorrect focus order. The WCAG criteria 2.1.1, 2.4.3, and 4.1.2 are all best tested manually.
How often do I need to re-audit popup apps?
Any time an app updates its frontend code, the accessibility patch may need updating too. Set a reminder to re-run the checklist after each app update and after any theme change that could affect script loading order. A popup that passed in March can regress in May if the app vendor rewrites their modal component.