Most cookie banners fail in the same three places: no "Reject" next to "Accept", cookies that last two years, and Google tags that do not know what the visitor chose. Each is a setting in your consent platform or a few lines of code. This guide shows what a banner that passes looks like, how the cookie scanner tests it, and the order to set it up in. Plan an hour to configure, and a second hour if tags have to move into Tag Manager.
Quick answer
- Put Reject all on the first layer, the same size and weight as Accept all, with a link to settings and to the privacy policy.
- Keep every consent-based cookie, including the one that stores the answer, at 13 months or less.
- Set Consent Mode v2 defaults to
deniedbefore any Google tag loads, and let the banner send the update. - Block every other tracker until consent, then test with the scanner: nothing before the click, nothing after "Reject".
- Give visitors a way back: a "Cookie settings" link in the footer that reopens the banner.
Why the banner's details matter
The rules in plain terms. In the EU, the ePrivacy rules say nothing may be stored on or read from a visitor's device before consent, unless it is strictly necessary for what they asked for. The GDPR defines consent: an active, informed, specific and free choice, as easy to withdraw as to give. Pre-ticked boxes and "by continuing to browse you agree" are not consent. The full legal picture, including the UK and the US, is in cookies before consent.
Refusing must be as easy as accepting. Regulators in several countries have required a reject option on the first layer. The French CNIL fined Google and Facebook in 2022 because refusing took more clicks than accepting, and in the European Data Protection Board's 2023 cookie banner taskforce report most EU authorities agreed that a first layer with "Accept" but no way to refuse breaks the rules. A banner with "Accept" and a small "×" or "Settings" is the most common failure.
Lifetimes should be proportionate. The French CNIL's guidance, the benchmark most banners and getReport's check follow, puts the maximum at 13 months; after that, ask again. For audience measurement that is exempt from consent, the CNIL's conditions keep the cookie to 13 months and the data collected with it to 25 months. Google Analytics' _ga lasts two years by default.
Consent Mode keeps the numbers usable. When half your visitors click "Reject", your analytics lose half the data. Google's Consent Mode tells Google tags what each visitor chose; in advanced mode, tags send cookieless pings when consent is denied, and Google uses them to model conversions and, on sites with enough traffic, behaviour in GA4. It does not replace the banner: without a banner asking, there is no consent to report.
How getReport checks it
The scanner loads the page in a fresh Chromium session, records every cookie and tracker request, finds the banner's accept button, clicks it, waits about 2.5 seconds and records what changed. Four findings come out of it that matter for the banner:

In that example, the tags wait for the click (every cookie says "after accept") and the lifetimes stay within 13 months. What fails is the banner itself: it offers no way to say no.
An info finding with no points. It runs only when a banner was detected. The scanner looks for a visible reject control, first through the known selectors of 18 consent platforms (Cookiebot's "Decline", OneTrust's "Reject All", CookieYes, Complianz, Usercentrics, Didomi and others), then for any visible button or link whose whole label is a reject phrase in about 20 languages, searching inside the banner first: "Reject all", "Decline", "Only necessary", "Continue without accepting", "Alle ablehnen", "Tout refuser", "Odbij sve" and so on. A reject button hidden behind "Settings" is not visible on the first layer, so it does not count.
Also info. It counts every cookie the scan saw, before and after the click and whatever its category, and lists those that live longer than 397 days. Chrome caps lifetimes at 400 days, so a two-year cookie shows as 400 days and is still listed.
Info again, and only shown when the page runs an analytics or marketing tag (GA4, Tag Manager, Meta Pixel, Hotjar, Clarity, Matomo, LinkedIn, TikTok and a few others). It passes when an inline script calls gtag('consent', …) or the dataLayer holds a consent command or an event whose name contains "consent". It also appears for cookieless analytics such as Plausible, where Consent Mode is irrelevant; ignore it there.
The one that costs points: a warning with weight 5. It matches requests made before the click against 25 analytics, advertising and session-replay trackers. When Consent Mode is present and no Google cookie was set before the click, Google's cookieless pings are not counted.
Step by step
1. Take the inventory
Run the scanner on the home page and on one page per template (a product, an article, the contact page). Write down every cookie and tracker with its owner and category. That list drives everything else: the banner's categories, the blocking rules and the cookie section of your privacy policy.
If the list shows no analytics or advertising cookies and no trackers, you may not need a banner at all. Session, cart and security cookies do not require consent.
2. Choose a consent platform
| Type | Examples | Good for |
|---|---|---|
| Open source, self-hosted | Klaro, tarteaucitron.js, CookieConsent (MIT licence) | Developers who want no third-party script and full control |
| Hosted platforms | Cookiebot, Usercentrics, CookieYes, Iubenda, OneTrust | Sites that want scanning, policy text and consent logs handled; several offer a free plan for small sites, with limits on pages or visits |
| WordPress plugins | Complianz, CookieYes, Cookie Notice | WordPress sites; the free versions on wordpress.org cover a basic banner and script blocking |
Whatever you choose, check four settings before anything else: a first-layer reject button, Consent Mode v2 support, script blocking by category, and the lifetime of the consent cookie.
3. Design the first layer
A banner that passes has, on the first screen:
- Two equal buttons: "Accept all" and "Reject all", same size, same colour weight. Grey text for reject next to a bright accept button is the pattern regulators object to.
- A "Settings" or "Choose" link that opens per-purpose toggles, all off by default.
- One or two sentences naming the purposes ("analytics to count visits, marketing to measure ads") and linking to the privacy policy.
- No cookie wall: the page stays readable while the banner is open.
The answer is remembered, and a "Cookie settings" link in the footer reopens the banner so visitors can change their mind as easily as they gave consent.
A minimal banner of your own, for a site whose only tags are Google's:
<!-- 1. First script in <head>, above gtag.js or Google Tag Manager -->
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
ad_storage: 'denied', ad_user_data: 'denied',
ad_personalization: 'denied', analytics_storage: 'denied',
wait_for_update: 500
});
if (/(?:^|; )site_consent=granted/.test(document.cookie)) {
gtag('consent', 'update', {
ad_storage: 'granted', ad_user_data: 'granted',
ad_personalization: 'granted', analytics_storage: 'granted'
});
}
</script><!-- 2. End of <body>: the banner, hidden once a choice exists -->
<div id="consent-banner" role="region" aria-label="Cookie consent" hidden>
<p>We use analytics and marketing cookies only if you agree.
<a href="/privacy-policy/#cookies">What we use</a></p>
<button type="button" id="consent-reject">Reject all</button>
<button type="button" id="consent-accept">Accept all</button>
</div>
<script>
(function () {
var banner = document.getElementById('consent-banner');
if (!/(?:^|; )site_consent=/.test(document.cookie)) banner.hidden = false;
function choose(granted) {
var v = granted ? 'granted' : 'denied';
gtag('consent', 'update', {
ad_storage: v, ad_user_data: v, ad_personalization: v, analytics_storage: v
});
// 180 days, well inside the 13-month limit
document.cookie = 'site_consent=' + v + '; Max-Age=15552000; Path=/; SameSite=Lax; Secure';
banner.hidden = true;
}
document.getElementById('consent-accept').addEventListener('click', function () { choose(true); });
document.getElementById('consent-reject').addEventListener('click', function () { choose(false); });
})();
</script>Style both buttons with the same class. A footer link that deletes site_consent and shows the banner again gives visitors their way back. This only covers Google's tags; any other tracker must be loaded after "Accept", which is what consent platforms automate.
4. Block everything else by category
Non-Google tags ignore Consent Mode. In Tag Manager, require consent in each tag's Consent Settings or fire it on your platform's consent event; in the page's HTML, use the platform's type="text/plain" pattern with a category attribute. Cookies before consent has the code for Cookiebot and OneTrust, and GA4 and Tag Manager: the cost and the consent covers the Google side in depth.
5. Set lifetimes
Shorten Google Analytics in GA4 (Admin → Data streams → your stream → Configure tag settings → Override cookie settings) or in the tag:
// gtag.js config (in Tag Manager: the Google tag's cookie_expires parameter), value in seconds
gtag('config', 'G-XXXXXXXXXX', { cookie_expires: 395 * 24 * 60 * 60 });In your consent platform, set the consent cookie's lifetime to 6 to 12 months; the check counts it like any other cookie.
6. Test before the click and after reject
Run the scanner again: no trackers before consent, the reject finding names your button, no cookie over 13 months. Then test "Reject" by hand, because the scanner only clicks accept: a new private window, open DevTools, click "Reject all", reload, open a second page. The Application tab should show no _ga, _fbp or similar cookies, and the Network tab no tracker requests beyond Google's cookieless pings.
The speed and UX cost
A consent platform is one more script on every page, with its configuration, and it has to run before your tags. Load one, not two. Show the banner as a fixed overlay at the bottom of the screen: an overlay does not move the page, while a banner inserted at the top of the document pushes everything down and shows up as layout shift. On mobile the banner text is often the largest element on screen, so it can become the page's Largest Contentful Paint; keep it short and use the page's own fonts. Fix Cumulative Layout Shift covers the shift side.
Platform notes
WordPress
Complianz, CookieYes and Cookie Notice all add a reject button through their banner settings; check it is switched on and visible on the first layer. Plugins that print their own tags (analytics, pixels, chat) need the consent plugin's integration or the WP Consent API, or they fire before consent regardless of the banner.
Shopify
The built-in banner (Settings → Customer privacy) offers accept and decline. Pixels added under Settings → Customer events follow the choice; code pasted into the theme does not.
Static sites and custom builds
Load order is what matters: consent defaults first, then the consent platform, then Tag Manager, with every other tag inside Tag Manager.
Verify
- The scanner shows "The cookie banner offers a reject option", "All … cookies expire within 13 months", "Analytics run with Google Consent Mode" and "No trackers load before consent".
- After "Reject all" by hand, no analytics or advertising cookies appear on the next two pages.
- The footer's "Cookie settings" link reopens the banner, and changing the answer takes effect without clearing cookies.
- The privacy policy lists the same cookies and services as the scanner; see privacy policy and contact links.
Common mistakes
- Accept and an ×, nothing else. Closing the banner is not refusing, and the scanner does not treat an × as a reject control. Add "Reject all" on the first layer.
- Cookies set by the theme or a plugin before consent. The banner is configured, but a hard-coded pixel in the header fires anyway. The trackers finding names it; remove the copy outside the consent platform.
- Consent stored for five years. The cookie holding the answer is subject to the same 13-month guidance, and the lifetime finding lists it. Set 6 to 12 months.
- A banner on a site with nothing to consent to. If the scan shows only necessary cookies and no trackers, the banner is friction without purpose. Remove it, or switch to cookieless analytics.
- A banner the scan cannot see. Banners drawn inside an iframe, shown only after a long delay, or hidden from headless browsers by a firewall rule leave the scan with the before-consent state only. The trackers result is still valid; check the reject button by hand.