In short: A cookie banner is the first thing every visitor has to operate. It must be reachable by keyboard, show visible focus, close without a mouse, and offer every choice equally readable and usable. As a modal it belongs in a <dialog> element with a heading; as a bar it must never cover whatever currently has focus.
Cookie banners are rarely anybody's favourite project. They come from a consent vendor or a plugin, get configured once and are forgotten. For accessibility that is a risk: a banner you cannot reach with the keyboard, or cannot close, makes the whole site unusable – however well the rest is built. Whether and for what you need consent is a matter for the GDPR and, in Germany, section 25 TDDDG. This guide is only about making the dialog accessible.
Why the banner is the most important element on your site
The banner appears on the first page somebody opens, and with many solutions it sits on top of the content until a choice is made. That makes it part of every page – and every barrier in it a barrier on every page. WCAG judges whole pages, not individual components: a home page with flawless content but a banner whose buttons cannot be reached does not meet the criteria.
The typical problems are always the same:
- The buttons cannot be reached with Tab, because the banner sits at the end of the source or the buttons are built as
divs. - Focus is invisible, because the vendor's theme sets
outline: none. - "Necessary only" is a light grey text link next to a large, coloured "Accept all".
- The banner is a fixed bar at the bottom and, while tabbing, covers exactly the links that have focus.
- The icon that reopens it is a cookie with no name.
Dialog semantics and focus (dialog, aria-modal, initial focus)
Decide first which kind of banner you have:
- A modal dialog blocks the page until a choice is made. Then it must behave like a dialog for assistive technology too: the rest of the page is unreachable, focus is inside the dialog, and the dialog has a name.
- A non-modal bar leaves the page usable and adds a region. Then it must not hold focus, but it must be reachable early in the tab order and must not cover anything.
For the modal case, the native <dialog> element is the most robust foundation. With showModal() the browser makes the rest of the page inert, puts the dialog in the top layer and sets the dialog role and modal behaviour itself.
<dialog id="consent" aria-labelledby="consent-title" aria-describedby="consent-text">
<h2 id="consent-title">Cookies and external content</h2>
<p id="consent-text">
We use necessary cookies to make this site work. With your consent we also
set analytics cookies.
<a href="/privacy/">Privacy policy</a>
</p>
<div class="consent-actions">
<button type="button" data-choice="all">Accept all</button>
<button type="button" data-choice="necessary">Necessary only</button>
<button type="button" data-choice="settings">Settings</button>
</div>
</dialog>
const dialog = document.getElementById('consent')
if (!localStorage.getItem('consent')) dialog.showModal()
// Escape closes a modal dialog. That is not consent:
// treat it as "Necessary only".
dialog.addEventListener('cancel', () => saveChoice('necessary'))
saveChoice stands for your own function that stores the choice. On initial focus: the browser moves focus into the dialog when it opens, and the autofocus attribute decides exactly where. Do not put it on "Accept all" – a screen reader user should first learn what this is about. A sensible start is the first button after the text, or the heading itself made focusable with tabindex="-1".
If your vendor does not use a native <dialog>, its container needs role="dialog", aria-modal="true" and a name via aria-labelledby, and the rest of the page must be unreachable while it is open. Rebuilding that reliably is harder than it looks – one more reason to ask for the native element.
Keyboard: tab order, Escape, no trap
A dialog that keeps focus inside is not a keyboard trap. It becomes one only when you cannot leave it by keyboard – neither with a button nor with Escape (2.1.2 No Keyboard Trap). So check that:
- Tab reaches every link and button in the banner, in an order that matches reading.
- Shift+Tab goes back.
- Enter and Space activate the buttons.
- Escape closes a modal dialog without granting consent.
- After the choice, focus lands somewhere sensible, typically the start of the page – not somewhere in the middle.
A non-modal bar has one more condition: it must sit at the start of the source, or be reachable through a link at the top of the page. A bar at the very bottom of the source is reached only after hundreds of Tabs. And it must not entirely hide focused elements while you tab (2.4.11 Focus Not Obscured). Setting scroll-padding-bottom to the bar's height on the html element makes the browser scroll focused elements into view above the bar.
html { scroll-padding-bottom: 6rem; } /* height of the fixed consent bar */
Equal buttons: contrast, size, labels
From an accessibility point of view the rule is simple: every choice must be equally perceivable and usable. Specifically:
- Contrast. Labels need at least 4.5:1 against their background (1.4.3 Contrast). Light grey on white rarely gets there. Check your colours with our contrast checker.
- Size. Targets should be at least 24 × 24 CSS pixels or have enough space around them (2.5.8 Target Size). A tiny "X" in the corner often fails this.
- Labels. "Accept all", "Necessary only" and "Settings" say what happens. "OK", "Continue" or a bare icon do not.
- Real buttons. Only
<button>brings role, focus and keyboard operation for free (4.1.2 Name, Role, Value). - Visible focus. Every button shows clearly when it has focus – on the banner's coloured background too.
Settings, categories and toggles
The second layer – categories such as analytics or marketing – is often built worse than the first. Toggles are divs with click handlers, have no name, or do not say whether they are on. A robust solution uses native checkboxes grouped in a fieldset:
<fieldset>
<legend>Categories</legend>
<div class="consent-option">
<input type="checkbox" id="c-necessary" checked disabled aria-describedby="d-necessary">
<label for="c-necessary">Necessary</label>
<p id="d-necessary">Always on. Stores, for example, your choice in this dialog.</p>
</div>
<div class="consent-option">
<input type="checkbox" role="switch" id="c-stats" aria-describedby="d-stats">
<label for="c-stats">Analytics</label>
<p id="d-stats">Helps us understand which pages are used.</p>
</div>
</fieldset>
<button type="button" data-choice="save">Save choices</button>
role="switch" makes screen readers announce "switch, off" rather than "checkbox, not checked"; Space toggles it like any checkbox. The description is tied to the toggle with aria-describedby. A long list of individual services can be made expandable with <details> and <summary>, without any script. And please: no saving on a mere toggle that closes the dialog unexpectedly.
The button that reopens it
Anybody who gave consent must be able to change it. Many vendors show a floating icon for this – often a cookie or a fingerprint in a corner. Three things commonly go wrong:
- The icon has no accessible name. A screen reader says "button" and nothing more.
- It covers content, such as the basket button in the mobile layout.
- It sits at the end of the tab order and is practically impossible to find.
A visible text button in the footer that reopens the dialog is better:
<footer>
…
<button type="button" class="link-button" id="consent-open">Cookie settings</button>
</footer>
document.getElementById('consent-open').addEventListener('click', () => dialog.showModal())
If there is a floating icon as well, it needs at least aria-label="Cookie settings" and must not cover other controls.
Questions for your consent vendor
Most sites do not build the banner themselves but use a tool – Borlabs Cookie, Usercentrics, Cookiebot or another. Your vendor should be able to answer these questions before you spend time on workarounds of your own:
- Does the dialog use a native
<dialog>, orrole="dialog"witharia-modaland a heading as its name? - Where does it put focus when it opens, and where after it closes?
- Can it be closed with Escape, and what is stored when it is?
- Are all buttons real buttons with visible focus?
- Can colours and sizes be adjusted so every button reaches 4.5:1 contrast and a 24-pixel target?
- Are the toggles in the settings native checkboxes, or buttons with
aria-pressedorrole="switch"? - Can the dialog be opened from a link of your own in the footer?
- Does the vendor have an accessibility audit report for the dialog, and against which WCAG version?
Answers that just say "it is accessible" are not answers. Ask for the report.
What Reviseberg checks here – and what it does not
Reviseberg clicks nothing while it crawls. It therefore sees your site the way a first-time visitor does: with the banner open. The axe-core rules run on the page, banner included, at 1280 pixels wide and, for the layout-dependent rules – contrast, target size – again at 360 pixels. Missing names, low contrast and small targets in the banner show up in the results.
The keyboard agent presses Tab, Shift+Tab and Escape on the page you name as its start page. It shows whether the banner can be reached, whether focus on its buttons is visible and whether you get stuck anywhere – and it deliberately does not report focus that stays inside a modal dialog as a trap. Reviseberg also lists which cookies were set while the page loaded, and which third-party hosts were contacted, before anybody could have used the banner. That is not a legal assessment.
What Reviseberg does not do: it neither opens the banner's settings nor checks what a choice does – the keyboard agent takes the most private choice, in a browser of its own, only to walk the page behind it. Test those steps by hand with the checklist above. More on the agent is under keyboard agent, every check is on the platform page. For WordPress sites with consent plugins, our WordPress guide helps as well.