Back to Blog
Your Cookie Banner Is the First Accessibility Failure Every Visitor Hits
GDPR forced cookie banners onto every European site. Most of them are accessibility disasters that block screen-reader users from your content entirely. Here is what breaks, what to fix, and how the major CMP vendors compare.
11 min read
Why cookie banners fail more than other UI
A normal modal on your site appears when the user does something — clicks a button, opens a menu. The user has context for it, they expect it, and they are usually focused on a related action.
A cookie banner appears the instant the page loads, while the user is mid-scroll-or-tab in pursuit of something completely different. The banner is structurally an interruption. To be accessible, it has to do four things at once that most UI components rarely have to do simultaneously:
- Steal focus in a way that does not feel like an attack
- Announce itself to assistive technology as a dialog requiring action
- Trap focus while open so the user cannot tab past it into hidden content
- Release focus correctly to the original content position when dismissed
Every one of these is a place where banner implementations break, and the failure modes compound.
The five most common failure modes
1. The banner is a <div>, not a <dialog>
WCAG 4.1.2, 1.3.1 · partial automation
The single most common pattern. The banner sits in the DOM as a styled <div> with position: fixed. Screen readers treat it as a content block, not a dialog. There is no announcement, no role, no understanding that the user must respond.
<!-- ❌ Bad: just a div, screen readers may not even announce its appearance -->
<div class="cookie-banner">
<p>We use cookies. Do you accept?</p>
<button>Accept</button>
<button>Reject</button>
</div>
<!-- ✅ Good: dialog with role, modal, and labelled -->
<div role="dialog" aria-modal="true" aria-labelledby="cookie-title" aria-describedby="cookie-desc">
<h2 id="cookie-title">Cookie consent</h2>
<p id="cookie-desc">We use cookies for analytics and personalisation. You can accept all, reject non-essential cookies, or customise your choices.</p>
<button>Accept all</button>
<button>Reject non-essential</button>
<button>Customise</button>
</div>
Native <dialog> element is also valid (and arguably better since it handles focus trap and ESC for free) — see the top 10 violations post for the native dialog pattern.
2. Focus is not trapped inside the banner
WCAG 2.4.3, 2.1.2 · manual testing required
The user tabs forward, expecting to navigate the banner. After three or four Tabs, focus escapes the banner into the hidden page behind it — sometimes visibly, sometimes not. The user is now interacting with content they cannot see, content that they have not given cookie consent for, and content that may not even be rendered if there is loading state.
The fix in vanilla JavaScript:
function trapFocus(dialog) {
const focusable = dialog.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
dialog.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
}
Native <dialog> with showModal() does this for free. If you use a framework, libraries like Radix UI, Headless UI, and the @react-aria/dialog hook all handle focus trapping.
3. Buttons have no accessible names
WCAG 4.1.2 · axe rule: button-name
Either the buttons are icon-only (a checkmark and an X), or they have generic text ("Accept" / "Reject") that doesn't say what is being accepted or rejected. Both fail.
<!-- ❌ Bad: icon-only, no name -->
<button class="accept-btn"><svg>...</svg></button>
<!-- ❌ Bad: ambiguous text -->
<button>OK</button>
<!-- ✅ Good: explicit, contextual -->
<button>Accept all cookies</button>
<button>Reject non-essential cookies</button>
<button>Customise cookie preferences</button>
4. The banner cannot be dismissed by keyboard
WCAG 2.1.1 · manual testing required
There is a close-X icon-button at the top right of the banner that only responds to mouse clicks. Keyboard users have no way to close the banner without first answering the consent question — which is fine, except many sites treat closing the banner as implicit acceptance, hidden behind the X.
The fix: Esc dismisses the banner, treated as "reject non-essential" (not as accept). Plus the close button is a real <button> with aria-label and keyboard handling.
<button aria-label="Close cookie banner without accepting">×</button>
dialog.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
rejectNonEssential();
closeBanner();
}
});
5. The banner blocks the rest of the page
WCAG 1.3.1, 4.1.2 · manual + automated
Even when the banner has the right ARIA roles, sometimes the rest of the page is left fully focusable. Tab from the banner and you land on links behind it that the user can read with a screen reader but cannot see (because the banner overlays them). Inversely, sometimes everything else has aria-hidden="true" applied incorrectly, hiding content from screen readers but also hiding it from sighted users in unintended ways.
The cleanest pattern is the native <dialog> element with showModal(), which handles inert state on the rest of the page automatically. Modern browsers (Chrome 105+, Firefox 110+, Safari 16+) all support it.
<dialog id="cookie-dialog" aria-labelledby="cookie-title">
<h2 id="cookie-title">Cookie consent</h2>
<!-- ... -->
</dialog>
<script>
document.getElementById('cookie-dialog').showModal();
</script>
For older browser support or richer customisation, use a tested library — listed below.
How the major CMP vendors compare in 2026
This is a snapshot; vendors update frequently. Verify your current configuration against your live banner. None of these is a guaranteed pass — your custom configuration is the live test.
| CMP | Default banner accessibility | Common gotchas |
|---|---|---|
| Cookiebot (Usercentrics) | Generally good in default templates; explicit accessibility documentation. | Custom CSS that overrides their built-in colours can break contrast; their "minimal" template strips out some labelled elements. |
| OneTrust | Adequate; depends heavily on template version and configuration. | Older templates lack proper role="dialog", multi-language switches not always announced. |
| Iubenda | Acceptable in 2026 default banners; older configurations weaker. | Per-purpose toggles in the "Customise" panel sometimes lack labels; check on second-level settings panel. |
| Cookieyes / Termly | Mixed; recent templates improved. | Default position (bottom strip) often pushed below screen-reader announcement order; verify placement in DOM. |
| Custom-built banners | Whatever you built it to be. | Almost always weaker than tested CMPs unless built by an accessibility-fluent team. |
Practical recommendation: if you can use a major CMP with their accessibility-tested default template, do that and resist customising the styling beyond the minimum. If your design team insists on heavy customisation, budget for an accessibility test of the resulting banner — every theme change is a regression risk.
The minimal accessible banner — copy-paste version
If you want to ship something solid quickly, here is a self-contained accessible cookie banner using the native <dialog> element. It is not a full CMP — you still need to wire up your specific cookie/storage logic — but the UI shell is sound.
<!-- HTML -->
<dialog id="cookie-banner" aria-labelledby="cookie-title" aria-describedby="cookie-desc">
<h2 id="cookie-title">Cookie preferences</h2>
<p id="cookie-desc">
We use essential cookies to make this site work. Optional cookies for
analytics help us understand how the site is used.
</p>
<div class="cookie-buttons">
<button id="accept-all" type="button">Accept all cookies</button>
<button id="reject-optional" type="button">Reject optional cookies</button>
<button id="customise" type="button">Customise</button>
</div>
</dialog>
/* CSS */
#cookie-banner[open] {
position: fixed;
bottom: 1rem;
left: 1rem;
right: 1rem;
max-width: 480px;
padding: 1.5rem;
border: 1px solid #ccc;
border-radius: 8px;
background: #fff;
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
/* ensure focus rings are visible */
outline: none;
}
#cookie-banner button {
padding: 0.6rem 1rem;
font: inherit;
cursor: pointer;
}
#cookie-banner button:focus-visible {
outline: 2px solid #0066ff;
outline-offset: 2px;
}
.cookie-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 1rem;
}
// JS
const dialog = document.getElementById('cookie-banner');
if (!localStorage.getItem('cookie-consent')) {
dialog.showModal();
}
document.getElementById('accept-all').addEventListener('click', () => {
localStorage.setItem('cookie-consent', 'all');
dialog.close();
// Initialise analytics, etc.
});
document.getElementById('reject-optional').addEventListener('click', () => {
localStorage.setItem('cookie-consent', 'essential-only');
dialog.close();
});
document.getElementById('customise').addEventListener('click', () => {
// Open second-level dialog for granular consent
});
// Esc closes the dialog and counts as "reject optional"
dialog.addEventListener('cancel', (e) => {
e.preventDefault(); // override default close
localStorage.setItem('cookie-consent', 'essential-only');
dialog.close();
});
This is the floor — not the ceiling. A proper CMP additionally handles per-vendor consent records, regional variation (different rules in different EU countries), prior consent storage with audit trail, and so on. But the UI shell above is more accessible than ~70% of the CMPs deployed in production today.
For the broader picture of why this matters, see the top 10 EAA violations, and for the per-country enforcement reality, the EAA fines guide and the France-specific enforcement notes.
How accessible is your cookie banner right now?
Webply's free scan tests your homepage with the cookie banner open, flags every missing label, missing dialog role, and unreachable focus state — and ranks each issue by EAA legal risk per country.
Sources
- European Data Protection Board — Guidelines 05/2020 on consent under GDPR.
- WAI-ARIA Authoring Practices — Modal Dialog pattern.
- MDN —
<dialog>element and HTMLDialogElement interface. - EN 301 549 v3.2.1 — chapter 9 (web content) and chapter 11 (software).
- French Auchan/Carrefour/E.Leclerc/Picard enforcement actions, November 2025.
The CMP comparison reflects market observation in 2026 and is not a vendor endorsement or warranty. Verify your live banner with assistive technology testing — vendor compliance status changes with each release.