AccessScanRun a free scan

Guide

How to Build an Accessible Cookie Banner

The cookie banner is the first thing many users meet on your site, and for keyboard and screen reader users it is often the first thing that breaks. An accessible cookie banner has to be reachable, operable, dismissible, and announced correctly, all before the user can read a word of your actual content. Get it wrong and you have built a wall in front of your homepage.

Most consent banners ship from a third-party CMP (consent management platform) or a quick in-house snippet, and most of them fail the same handful of WCAG 2.2 checks. This guide walks through the concrete failures developers introduce, the markup and focus behaviour that fixes them, and how to verify the result. It assumes you are comfortable in HTML, CSS, and JavaScript.

Decide whether the banner is a dialog or a region

The first design question determines everything else: does the banner block the page, or does it sit alongside content? If users can still scroll and interact with the page behind it, the banner is a non-modal region and should not trap focus. If it visually and functionally blocks the page until a choice is made, it is a modal dialog and must follow modal semantics.

For a non-modal banner, wrap it in a landmark with an accessible name, for example a container with role="region" and aria-label="Cookie consent", and place it near the end of the DOM. For a true blocking banner, use role="dialog" with aria-modal="true" and an aria-labelledby pointing at the heading. Do not mix the two: a region that visually blocks everything but lets focus escape to a page the user cannot see is the worst of both worlds.

A practical recommendation: prefer the non-modal region unless your legal team genuinely requires a hard block. Non-modal banners are far less likely to trap users and far easier to make compliant.

Use real buttons, not styled divs

The single most common failure is fake buttons. A consent control built from a clickable <div> or <a href="#"> with a JavaScript handler is invisible to keyboard users and misannounced by screen readers. "Accept all", "Reject all", and "Manage preferences" are actions, so they must be <button> elements.

Native <button> gives you keyboard activation (Enter and Space), the correct "button" role, focusability, and disabled-state semantics for free. If you must restyle, reset with appearance and keep the element:

  • Right: <button type="button">Reject all</button> styled with CSS.
  • Wrong: <div class="btn" onclick="reject()">Reject all</div> with no role, no tabindex, no key handling.
  • Wrong: an <a> with role="button" bolted on, which still needs manual Space-key handling and a real action target.

Equal prominence matters too. If "Accept all" is a large coloured button and "Reject all" is faint grey text, that is a dark pattern and, in the EU, a likely consent-validity problem under data protection guidance. Give accept and reject the same visual weight and the same control type.

Keyboard operability, focus order, and no traps

Every control in the banner must be reachable and operable with the keyboard alone, satisfying 2.1.1 Keyboard. Tab should move through the buttons and any links in a logical order, Enter and Space should activate them, and there should be no control that only responds to a mouse click or hover.

For a modal banner, set initial focus deliberately when it opens, usually to the heading or the first interactive control, not left on the body. When the user makes a choice and the banner closes, return focus to a sensible place, typically the element that had focus before, or the top of the main content. Never let focus vanish to <body>, which strands keyboard and screen reader users with no visible position.

A focus trap you cannot escape is a 2.1.2 No Keyboard Trap failure, and it is depressingly common in cookie banners. Match the containment to the type. For a modal banner, mark everything outside the dialog as inert (the inert attribute, or aria-hidden plus removing those elements from the tab order), so Tab and Shift+Tab cycle only within the dialog and Escape closes it. Inert is preferable to a hand-rolled Tab loop because it also blocks pointer and screen-reader virtual-cursor access to background content, not just the Tab key.

For a non-modal region, do the opposite: do not manage Tab at all. Let focus flow naturally out of the banner into the page. If you globally hijack the Tab key on a non-modal banner, you can accidentally prevent users from reaching the rest of the page. Test both directions, because a trap often only reveals itself on Shift+Tab.

Visible focus, target size, and contrast

Keyboard operability is useless if users cannot see where they are. Every button and link in the banner needs a clear focus indicator, satisfying 2.4.7 Focus Visible. Do not ship the common CSS reset that kills outlines: a blanket outline: none on :focus removes the only cue many users have. Use :focus-visible to show a strong indicator for keyboard users, and make sure it has enough contrast against the banner background.

WCAG 2.2 also adds 2.4.11 Focus Not Obscured (Minimum, AA): a focused control must not be entirely hidden behind other content. A sticky banner pinned to the bottom of the viewport can cover a focused element elsewhere on the page, so account for the banner's height when content scrolls into focus. And under 2.5.8 Target Size (Minimum, AA), interactive targets should be at least 24 by 24 CSS pixels, with adequate spacing, so a tiny close X in the corner is a real failure.

Banner text needs a contrast ratio of at least 4.5:1 against its background for normal text, and 3:1 for large text. Button borders, focus rings, and the boundary between a control and its surroundings are user interface components and need 3:1 under 1.4.11 Non-text Contrast. A pale grey "Reject" link on white is a contrast failure as well as a dark pattern. Run your palette through a contrast checker before shipping.

Survive zoom, reflow, and assistive tech

The banner must survive zoom and custom text settings. At 200% zoom (1.4.4 Resize Text) and when reflowed to a 320 CSS pixel width (1.4.10 Reflow), buttons must not overlap or get clipped, and text must not be cut off. Test text spacing too (1.4.12 Text Spacing): if a fixed-height banner clips its own text when line height increases, that is a failure. Avoid fixed pixel heights on the banner container and let it grow.

A banner can be perfectly operable and still wall off your site. If a modal banner sets aria-hidden="true" or inert on the rest of the page but never lets the user dismiss it without consenting, screen reader users are stuck on a single dialog. Always provide a reachable, clearly labelled way out, accept and reject at minimum, and make sure dismissing the banner restores access to the page.

Announce the banner correctly. For a modal, role="dialog" plus aria-modal and an accessible name does the work when you move focus into it. For a non-modal region that appears after load, consider whether it needs a status announcement; if you use a live region, keep it polite (aria-live="polite"). Do not slap role="alert" or aria-live="assertive" on the whole banner, which can hijack the screen reader and re-announce on every change, a 4.1.3 Status Messages problem rather than a fix. And do not let the banner steal focus on every page load or re-render: a banner that grabs focus repeatedly, or re-mounts and resets the user's position, is as disabling as a trap.

Verify it, do not assume it

Cookie banners are mostly broken by hand-coded shortcuts, so test by hand. Unplug the mouse and run the whole flow: Tab to each control, activate with Enter and Space, Shift+Tab back, press Escape, and confirm focus lands somewhere sensible afterwards. Then run it with a screen reader (VoiceOver, NVDA, or both) to hear how the banner is announced and whether you can reach the page behind it.

Automated tooling catches the structural failures fast, missing button semantics, low contrast, missing names, before you get to manual testing. Run a free scan with AccessScan to flag those, then use the checks above to confirm the keyboard and focus behaviour that automation cannot fully judge. A clean banner is a small amount of code; an inaccessible one undermines everything behind it.

Check your site against AccessScan

See your issues ranked by impact in seconds — free.

Run a free accessibility scan

FAQ

Is an accessible cookie banner legally required?

In the EU, two regimes overlap. Data protection law governs consent itself (it must be freely given, which is why accept and reject need equal prominence). Accessibility law, via the European Accessibility Act and EN 301 549, requires the banner to meet WCAG 2.2 Level A and AA like any other interface on an in-scope site. A banner that keyboard users cannot operate fails both the spirit of valid consent and the accessibility baseline.

Should a cookie banner be a modal dialog?

Only if it genuinely blocks the page. A non-modal region (role="region" with an accessible name) that lets users keep reading is easier to make accessible and far less likely to trap focus. Reserve role="dialog" with aria-modal="true" and inert background content for cases where your legal requirements truly demand a hard block.

My consent platform (CMP) generates the banner. Am I off the hook?

No. The site owner is responsible for the rendered result, not the vendor. Many CMPs ship fake buttons, weak contrast, or focus traps out of the box. Audit the actual DOM the CMP injects, keyboard-test it, and use the CMP's theming options (or override its CSS) to fix focus indicators, contrast, and target sizes.

How do I stop the banner trapping keyboard focus?

Match the containment to the type. For a modal banner, mark everything outside it as inert so Tab cycles only inside and Escape closes it. For a non-modal banner, do not manage Tab at all, let focus flow naturally into the page. Then test both Tab and Shift+Tab, since traps often only appear when tabbing backwards.

More guides