Search topics
Cookie banners that keyboard users reach last
A consent script adds the banner after the footer and fixes it to the bottom of the window, so it is drawn in front of the page but comes last in the order Tab and screen readers follow.
document.body.append(banner) The problem
Keyboard and screen reader users meet the cookie banner only after the whole page, and it hides part of the page from them on the way.
The home page of Harbour Museum has a header with four links (Visit, Exhibitions, Events, Shop), six links in the page (Opening hours, Book a guided tour, Family trail, Ship models gallery, Café menu, Access guide) and three in the footer (Contact, Press, Jobs). On a first visit, a cookie banner covers the bottom of the window: "We use cookies to count visits and improve the site", with Accept all, Reject all and Choose cookies. It is the first thing a sighted mouse user sees. Press Tab from the top of the page: focus goes through the menu, the page and the footer, and reaches Accept all on the fourteenth press. On the way, the links that scroll to the bottom of the window go behind the banner, focused but out of sight. A screen reader reading from the top reads the footer before it reads the banner.
The consent script builds the banner when the page loads and adds it with document.body.append(), after the footer. CSS then fixes it to the bottom of the window. Where it is drawn and where it sits in the source have nothing to do with each other, and Tab and screen readers follow the source. Success criterion 2.4.3 asks that focus moves in an order that keeps the page usable. The banner is drawn in front of everything and covers part of every screen until it is answered, yet it comes after everything, so a keyboard user has to work through the page with part of it hidden before they can clear it. Success criterion 2.4.11 fails on the way: links that scroll under the banner are entirely hidden while they have focus.
On a phone the banner can fill half the screen, so more of the page sits under it and more Tab presses pass behind it.
3 of 10 groups affected
- No vision (affected)
- Low vision (affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (affected)
- Reach, strength (not affected)
- Cognitive (not affected)
- Seizures (not affected)
- Limited manipulation Keyboard users press Tab through every link on the page before they can answer, and some of those links pass behind the banner on the way, focused but out of sight.
- Without vision Screen reader users reading from the top hear the banner only after the footer, if they get that far, so they may not know a choice is waiting or that part of the screen is covered.
- Limited vision People who zoom in see the banner take up much more of the window, so more of the page sits under it while they tab towards it.
Each button has a name and the banner has text, so a scan of the page passes it. The order is only wrong compared with where the banner is drawn, and source-order checks do not compare the two. The banner also only appears on a first visit, without a saved choice: a scanner that keeps cookies, or runs after someone accepted, never sees it. Most banners come from a third-party consent platform whose code nobody on the team reads. Generated banner code tends to follow the common tutorial pattern: build the element, append it to the body, fix it to the bottom.
Try it
Press Tab from the top of the page until you reach the cookie banner.
Count the presses, and watch the links near the bottom of the page: do any slip behind the banner? For the consent dialog, press Open the page first.
Broken Can you fix this?
Welcome to the harbour
A recreated demo. The broken version is intentionally inaccessible.
The fix
Put the banner first in the page, or open it as a modal dialog that Escape closes, so it is the first thing anyone reaches.
- 1On a new visit, the banner is the first thing Tab and a screen reader reach.
- 2While it shows, it never hides the focused control.
- 3Leaving it takes Tab, an answer or Escape, never a reload.
<body> <header class="site-header">…</header> <main>…</main> <footer>…</footer> <div class="cookie-banner"> <p>We use cookies to count visits and improve the site.</p> <button type="button">Accept all</button> <button type="button">Reject all</button> <a href="/cookies">Choose cookies</a> </div></body>The banner first in the page
<body> <section class="cookie-banner" aria-labelledby="cookie-title"> <div class="cookie-question"> <h2 id="cookie-title">Cookies on Harbour Museum</h2> <p>We use cookies to count visits and improve the site.</p> <button type="button" value="accept">Accept all</button> <button type="button" value="reject">Reject all</button> <a href="/cookies">Choose cookies</a> </div> <p class="cookie-saved" tabindex="-1" hidden> Your cookie choice is saved. You can change it on the cookies page. </p> </section> <header class="site-header">…</header> <main>…</main> <footer>…</footer></body>The banner is the last element in the body, so it is the last stop for Tab and the last thing a screen reader reads, while position: fixed draws it over the bottom of the window from the moment the page loads.
The banner is the first thing in the body, so it is the first Tab stop and the first thing a screen reader reads, and the section with a heading gives it a name in the landmarks list. It sits in the page flow at the top instead of floating over it, so it cannot hide focus anywhere. After a choice, focus moves to the confirmation in the same place, so nobody is dropped at an unknown point. If a script builds the banner, add it with document.body.prepend(banner) instead of append().
The banner first in the page and the consent dialog both pass. The banner first is the smaller change: people can read and use the page and answer when they are ready, and nothing needs managing once it is in the right place. Its cost is that it pushes the page down on a first visit. The dialog makes everyone answer before using the page, and the browser handles focus, the inert background and Escape. Its cost is that it blocks the page for everyone, screen reader users included, before they have heard what the page is. Choose the banner first unless the page really has to wait for an answer. Reserving the banner's height on its own is a stopgap for a banner you cannot move: it keeps focus visible, but the banner stays last.
Most cookie banners are added by a consent management platform, so the markup is not yours to edit. Work through these in order, and stop at the first one that works.
- Check the platform's settings. Many let you choose the layout (a bar at the top, a bar at the bottom, a modal) and where in the page the banner goes. Choose a bar inserted at the start of the body, or a modal that closes on Escape.
- If there is no such setting, move the banner yourself once it appears: watch the body with a
MutationObserver, and when the banner's element is added, move it withdocument.body.prepend(). Disconnect the observer after the move, and check that the banner's buttons still work, because some scripts rebuild the banner and put it back where it was. - If the banner cannot be moved, reserve its height with
scroll-padding-bottom, as in the last fix, so focus at least stays in view. Treat that as a stopgap, not the end of the work. - Report the problem to the vendor, with the steps to reproduce it, and ask for their accessibility conformance report. Until it is fixed, list it as a known issue in your accessibility statement.
Check the banner for focus traps while you are there. They are common in vendor code, and they fail 2.1.2 (No Keyboard Trap).
- A banner that does not block the page, but whose script loops Tab inside it until a choice is made.
- A consent dialog that cancels Escape and offers only Accept as a way out.
- A "Choose cookies" panel that opens over the banner and, when closed, drops focus at the top of the document instead of returning it.
The keyboard-traps and modal-focus-management guides cover these in detail. Vendor scripts change without a deploy on your side, so repeat the steps under "Verify the fix" after each vendor update and each change to the platform's settings.
It can look like a shortcut: call focus() on the banner's first button when the page loads, and every keyboard user starts there. For a banner that does not block the page, don't. It is not a failure of a success criterion on its own, but placing the banner first in the source already makes it the first Tab stop and the first thing read, and forcing focus there takes away more than it gives.
- Screen reader users lose their bearings. When a page loads, a screen reader announces its title and starts at the top. Moving focus jumps straight to a button, often cutting the title off, so people hear "Accept all, button" without knowing which page they are on.
- People who use screen magnification have their view pulled to wherever the banner is drawn, often the bottom of the window, away from the top of the page they expected to see.
- Keyboard users who want to read first have to deal with the banner before anything else, instead of choosing when to answer it.
The exception is the consent dialog. When the page cannot be used until the question is answered, focus has to move into the dialog, and showModal() does that. Some consent platforms move focus to their banner by default: look for a setting that turns it off, and if there is none, include it in your report to the vendor.
Put the banner before the skip link, as the GOV.UK Design System does. The first Tab then reaches the banner and the second the skip link, and once the banner is answered the skip link is first again. The same guidance advises against fixing the banner to the screen at all, because a fixed banner can cover focused content. If your design keeps it fixed to the bottom, keep it first in the source and reserve its height, as in the focus-not-obscured guide.
Render the banner in the server's HTML when you can. It is then in place before any script runs, and it does not push the page down after people have started reading.
Fixing this with an AI coding assistant? Get this guide as Markdown
Verify the fix
5 checks, no mouse.
Test with: Keyboard, Screen reader, Zoom
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the consent platform or its settings change.
Limits
This is a starting pattern, not a guarantee for every application. It covers how the banner is reached and what it hides, not what counts as valid consent: decide with whoever owns consent what each choice saves, and whether Escape may leave the question unanswered. Auditors differ on whether a banner placed last fails 2.4.3 on its own, but the focus hidden behind it fails 2.4.11 whenever it happens. Where Tab carries on after a banner or dialog is removed differs between browsers, so check it in the ones you support.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
Keep this fixed as your code changes.
Turn guides like this into rules your coding agents and CI follow, then have the result retested with a keyboard and screen readers.