Accessibility pattern · Forms and flows

Cookie consent

The banner is a labelled region at the top of the page, in the normal flow: it pushes the page down rather than covering it, so it can never hide what has focus, and it never traps the keyboard. Accept all, Reject all and Manage choices carry the same weight; after a choice, focus moves to the confirmation, and Cookie settings at the foot of the page reopens the choices at any time.

WCAG criteria
6
Keyboard rules
5
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabReaches the banner's three buttons first, then the page; the banner never holds focus.
Enter or SpaceOn Accept all or Reject all, records the choice and moves focus to the confirmation in the banner.
Enter or SpaceOn Manage choices or Cookie settings, opens the choices dialog with focus on the first switch you can change.
SpaceOn a switch, turns that purpose on or off. Necessary stays on and says why.
EscapeCloses the choices dialog without saving and returns focus to the button that opened it.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the bannerCookies on Kesar Tea, region. Accept all, button
Reject all is pressedYou have rejected optional cookies., group. You can change your choices at any time with Cookie settings, at the foot of every page.
The choices dialog opensCookie choices, dialog. Choose which cookies Kesar Tea may use. Analytics, switch, off. Count visits and see which pages are used, so we can improve the shop.
Focus reaches NecessaryNecessary, switch, on, unavailable. Keep the shop working: your basket, signing in and security. They cannot be turned off.
Choices are saved from Cookie settingsYour cookie choices are saved.

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-cookie-consent" data-ap-cookie-consent>
  <div class="ap-cookie-consent__window">
    <div class="ap-cookie-consent__bar" aria-hidden="true"><span></span><span></span><span></span></div>
    <div class="ap-cookie-consent__page" data-ap-page>
      <section class="ap-cookie-consent__banner" aria-labelledby="cc-banner-name" data-ap-banner>
        <div class="ap-cookie-consent__ask" data-ap-ask>
          <h3 class="ap-cookie-consent__name" id="cc-banner-name">Cookies on Kesar Tea</h3>
          <p class="ap-cookie-consent__text">We use necessary cookies to make this shop work. With your permission, we would also like to count visits and show you offers that suit you.</p>
          <div class="ap-cookie-consent__choices">
            <button type="button" class="ap-btn" data-ap-accept>Accept all</button>
            <button type="button" class="ap-btn" data-ap-reject>Reject all</button>
            <button type="button" class="ap-btn" data-ap-manage aria-haspopup="dialog">Manage choices</button>
          </div>
        </div>
        <div class="ap-cookie-consent__done" role="group" aria-labelledby="cc-done-text" aria-describedby="cc-done-more" tabindex="-1" data-ap-done hidden>
          <svg class="ap-cookie-consent__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.6 2.6L16 9.6"/></svg>
          <div>
            <p class="ap-cookie-consent__outcome" id="cc-done-text" data-ap-outcome></p>
            <p class="ap-cookie-consent__text" id="cc-done-more">You can change your choices at any time with Cookie settings, at the foot of every page.</p>
          </div>
          <button type="button" class="ap-btn" data-ap-hide>Hide this message</button>
        </div>
      </section>

      <div class="ap-cookie-consent__site">
        <a class="ap-cookie-consent__brand" href="#cc-home" data-ap-home>Kesar Tea Co.</a>
        <nav class="ap-cookie-consent__nav" aria-label="Kesar Tea">
          <a href="#cc-teas">Teas</a>
          <a href="#cc-gifts">Gifts</a>
          <a href="#cc-gardens">Our gardens</a>
        </nav>
      </div>
      <div class="ap-cookie-consent__hero">
        <h3 class="ap-cookie-consent__hero-name">First flush Darjeeling is here</h3>
        <p>Picked in March near Kurseong. Light, floral, and best without milk.</p>
        <a class="ap-btn ap-btn--primary" href="#cc-first-flush">Shop first flush</a>
      </div>
      <ul class="ap-cookie-consent__teas">
        <li><svg class="ap-cookie-consent__leaf" viewBox="0 0 48 32" aria-hidden="true" focusable="false"><path d="M24 4c9 4 12 12 7 19-3 4-9 5-13 2-4-4-3-14 6-21Z"/><path d="M17 27c3-7 6-12 11-17"/></svg><a href="#cc-darjeeling">Darjeeling first flush</a><span>100 g · ₹780</span></li>
        <li><svg class="ap-cookie-consent__leaf" viewBox="0 0 48 32" aria-hidden="true" focusable="false"><path d="M24 4c9 4 12 12 7 19-3 4-9 5-13 2-4-4-3-14 6-21Z"/><path d="M17 27c3-7 6-12 11-17"/></svg><a href="#cc-assam">Assam second flush</a><span>250 g · ₹540</span></li>
        <li><svg class="ap-cookie-consent__leaf" viewBox="0 0 48 32" aria-hidden="true" focusable="false"><path d="M24 4c9 4 12 12 7 19-3 4-9 5-13 2-4-4-3-14 6-21Z"/><path d="M17 27c3-7 6-12 11-17"/></svg><a href="#cc-nilgiri">Nilgiri frost tea</a><span>100 g · ₹460</span></li>
      </ul>
      <div class="ap-cookie-consent__foot">
        <a href="#cc-privacy">Privacy</a>
        <button type="button" class="ap-cookie-consent__settings" data-ap-settings aria-haspopup="dialog">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M20.5 12.5a8.5 8.5 0 1 1-9-9 3 3 0 0 0 4 3.5 3 3 0 0 0 5 5.5Z"/><path d="M8.5 9.5h.01M8 14.5h.01M12.5 16h.01M15.5 12.5h.01"/></svg>
          Cookie settings
        </button>
        <p class="ap-cookie-consent__saved" role="status"></p>
      </div>
    </div>
  </div>

  <dialog class="ap-cookie-consent__dialog" aria-labelledby="cc-dialog-name" aria-describedby="cc-dialog-desc">
    <form class="ap-cookie-consent__form" method="dialog">
      <div class="ap-cookie-consent__top">
        <h2 class="ap-cookie-consent__dialog-name" id="cc-dialog-name">Cookie choices</h2>
        <button type="button" class="ap-cookie-consent__close" data-ap-close aria-label="Close">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18"/></svg>
        </button>
      </div>
      <p class="ap-cookie-consent__dialog-desc" id="cc-dialog-desc">Choose which cookies Kesar Tea may use. Nothing optional is on until you turn it on.</p>
      <ul class="ap-cookie-consent__purposes">
        <li class="ap-cookie-consent__purpose ap-cookie-consent__purpose--locked">
          <div class="ap-cookie-consent__purpose-text">
            <label class="ap-cookie-consent__purpose-name" for="cc-necessary">Necessary</label>
            <p class="ap-cookie-consent__purpose-desc" id="cc-necessary-desc">Keep the shop working: your basket, signing in and security. They cannot be turned off.</p>
          </div>
          <span class="ap-cookie-consent__control">
            <input class="ap-cookie-consent__switch" type="checkbox" role="switch" id="cc-necessary" name="necessary" aria-describedby="cc-necessary-desc" checked aria-disabled="true" />
            <span class="ap-cookie-consent__track" aria-hidden="true"><span class="ap-cookie-consent__thumb"></span></span>
            <span class="ap-cookie-consent__state" aria-hidden="true"><span>Always on</span></span>
          </span>
        </li>
        <li class="ap-cookie-consent__purpose">
          <div class="ap-cookie-consent__purpose-text">
            <label class="ap-cookie-consent__purpose-name" for="cc-analytics">Analytics</label>
            <p class="ap-cookie-consent__purpose-desc" id="cc-analytics-desc">Count visits and see which pages are used, so we can improve the shop.</p>
          </div>
          <span class="ap-cookie-consent__control">
            <input class="ap-cookie-consent__switch" type="checkbox" role="switch" id="cc-analytics" name="analytics" aria-describedby="cc-analytics-desc" autofocus />
            <span class="ap-cookie-consent__track" aria-hidden="true"><span class="ap-cookie-consent__thumb"></span></span>
            <span class="ap-cookie-consent__state" aria-hidden="true"><span class="ap-cookie-consent__on">On</span><span class="ap-cookie-consent__off">Off</span></span>
          </span>
        </li>
        <li class="ap-cookie-consent__purpose">
          <div class="ap-cookie-consent__purpose-text">
            <label class="ap-cookie-consent__purpose-name" for="cc-preferences">Preferences</label>
            <p class="ap-cookie-consent__purpose-desc" id="cc-preferences-desc">Remember choices like your currency and the teas you looked at.</p>
          </div>
          <span class="ap-cookie-consent__control">
            <input class="ap-cookie-consent__switch" type="checkbox" role="switch" id="cc-preferences" name="preferences" aria-describedby="cc-preferences-desc" />
            <span class="ap-cookie-consent__track" aria-hidden="true"><span class="ap-cookie-consent__thumb"></span></span>
            <span class="ap-cookie-consent__state" aria-hidden="true"><span class="ap-cookie-consent__on">On</span><span class="ap-cookie-consent__off">Off</span></span>
          </span>
        </li>
        <li class="ap-cookie-consent__purpose">
          <div class="ap-cookie-consent__purpose-text">
            <label class="ap-cookie-consent__purpose-name" for="cc-marketing">Marketing</label>
            <p class="ap-cookie-consent__purpose-desc" id="cc-marketing-desc">Show you Kesar Tea offers on other sites, and measure them.</p>
          </div>
          <span class="ap-cookie-consent__control">
            <input class="ap-cookie-consent__switch" type="checkbox" role="switch" id="cc-marketing" name="marketing" aria-describedby="cc-marketing-desc" />
            <span class="ap-cookie-consent__track" aria-hidden="true"><span class="ap-cookie-consent__thumb"></span></span>
            <span class="ap-cookie-consent__state" aria-hidden="true"><span class="ap-cookie-consent__on">On</span><span class="ap-cookie-consent__off">Off</span></span>
          </span>
        </li>
      </ul>
      <div class="ap-cookie-consent__actions">
        <button type="button" class="ap-btn" data-ap-close>Cancel</button>
        <button type="submit" class="ap-btn ap-btn--primary" value="save">Save choices</button>
      </div>
    </form>
  </dialog>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The banner is a region named by its heading, and each switch is labelled by its purpose and described by what it does.

  • 2.1.2 No Keyboard Trap Level A

    The banner is not modal: Tab moves past it into the page, and the page works while it is showing.

  • 2.4.3 Focus Order Level A

    Focus moves to the confirmation after a choice, back to the opener when the dialog closes, and on to the shop when the message is hidden.

  • 2.4.11 Focus Not Obscured (Minimum) Level AA

    The banner sits in the page's flow, so it pushes content down instead of covering it. Pinned to the bottom, it reserves its height with scroll-padding, so focused links scroll clear of it.

  • 4.1.2 Name, Role, Value Level A

    Each purpose is a checkbox with role=switch, so it is announced as on or off; Necessary is aria-disabled and stays in the Tab order so its reason is heard.

  • 4.1.3 Status Messages Level AA

    Choices saved from Cookie settings are confirmed in a status message, without moving focus.

Usage

When to use it

Use it

  • Asking for consent to optional cookies or tracking, where the law or your policy needs a free and informed choice.
  • Any one-time notice that should not block the page, but must be easy to find and answer.

Use something else

  • Sites that set only necessary cookies: you need no banner, just a cookie notice in the privacy page.
  • A modal that blocks the page until people agree; it pressures the choice and traps keyboard users.
  • Messages about the page itself, like errors or offers: use an alert or a callout.

Common failures

How it usually goes wrong

  • A banner that covers the focused element

    A fixed banner over the bottom of the screen hides the links people tab to, failing 2.4.11. This one is part of the page flow, or reserves its height when pinned.

  • Reject hidden behind a link

    A bright Accept and a faint Reject link pushes people towards agreeing and is hard to find for anyone using a magnifier. All three choices here look and act the same.

  • A focus trap until you agree

    Making the banner modal forces a decision before people can read what they are agreeing to. The banner here is a region; only the Manage dialog is modal.

  • Disabled switches with no reason

    A greyed-out switch that cannot take focus is skipped by keyboard users and never explained. Necessary stays focusable, says it is unavailable, and its description says why.

  • No way to change your mind

    Consent has to be as easy to withdraw as to give. Cookie settings at the foot of the page reopens the same choices.

  • Focus lost when the banner goes

    Removing the button that has focus drops people at the top of the document. Focus moves to the confirmation, then to the shop's first link when it is hidden.

Notes

Building it

  • Put the banner first in the page, before the site header, so it is the first thing a screen reader or the Tab key reaches. It is a section with aria-labelledby, which makes it a named region.
  • Pinned to the bottom (the option above), the banner is still first in the source; a ResizeObserver writes its height into scroll-padding-bottom on the scrolling page, so the browser scrolls a focused link above it.
  • Necessary uses aria-disabled rather than disabled, so it stays in the Tab order and reads its explanation; the script stops clicks from changing it.
  • This demo keeps the choice in memory only, and the shop links point to #… anchors. A real site stores the choice, sets no optional cookies before it, and reads it on every page.

Sources: Understanding SC 2.4.11: Focus Not Obscured (Minimum) · GOV.UK Design System: Cookie banner · WAI-ARIA Authoring Practices: Switch

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction