--- name: cookie-consent-banner description: Build a GDPR/ePrivacy-compliant cookie consent banner with correct Google Tag Manager and GA4 integration. Use this skill whenever the user asks about cookie banners, consent management, CMP implementation, GDPR consent mode, Google Consent Mode v2, cookie compliance, or wants to add/fix/improve consent handling on a website. Also triggers for "my analytics numbers dropped after adding a consent banner" or "how do I set up consent mode in GTM." --- # Cookie Consent Banner Build a cookie consent banner that is legally compliant, doesn't tank your analytics, and actually works with Google Tag Manager and GA4. Most cookie banners on the web are broken — this skill produces banners that get it right. ## Why Most Cookie Banners Are Broken Before building, understand the three ways consent banners typically fail: 1. **The "decoration" banner** — Shows a banner but fires all tags regardless of choice. Illegal under GDPR/ePrivacy and increasingly enforced with real fines. 2. **The "nuke everything" banner** — Blocks all tracking until consent, but never sends Google Consent Mode defaults. Google has zero signal, modeling doesn't kick in, and you lose 40-70% of your data permanently. 3. **The "consent mode theater" banner** — Sets consent mode defaults but then grants consent for everything on page load before the user actually clicks. Still illegal. The correct implementation sends Google Consent Mode v2 defaults on page load (before any tags fire), waits for real user interaction, and then updates consent state accordingly. ## Architecture: The 4-Phase Consent Flow These four phases must happen in exact order. If the order is wrong, your implementation is broken. ``` Phase 1: DEFAULTS (before GTM loads) → Set consent mode defaults to "denied" Phase 2: GTM LOADS → Container loads, tags see denied state Phase 3: USER INTERACTS → User clicks accept/reject/customize Phase 4: UPDATE → Consent state updates, tags fire (or don't) ``` This ordering is non-negotiable. If GTM loads before defaults are set, tags fire in an undefined consent state — both a legal and data quality problem. ## Step-by-Step Implementation ### Step 1: Consent Mode Defaults Place this script in ``, BEFORE the GTM snippet. This is the most common mistake — if this comes after GTM, the entire implementation is broken. ```html ``` **Why `wait_for_update: 500`?** This tells Google tags to wait up to 500ms for a consent update before using the defaults. This gives the banner JS time to check for a previously stored consent choice (returning visitor) and call `gtag('consent', 'update', ...)` before tags fire. Without this, returning visitors who already consented still get denied on the first pageview. **Why `security_storage` is always granted:** This covers security features, authentication, and fraud prevention. It does not set tracking cookies. ### Step 2: GTM Container Snippet Place the standard GTM snippet immediately after the consent defaults script. No changes needed to the GTM snippet itself — consent mode is handled via the dataLayer. ### Step 3: Banner UI and Consent Logic Build a banner that: - Appears only when no prior consent choice is stored - Offers at minimum: Accept All, Reject All, and a way to customize per category - Stores the choice in a first-party cookie (not localStorage — cookies are more reliable and travel with requests) - Pushes a `gtag('consent', 'update', ...)` call on interaction - Pushes a custom dataLayer event so GTM can trigger tags conditionally #### Cookie Helper Functions ```javascript function setConsentCookie(consentState) { const value = JSON.stringify(consentState); const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString(); document.cookie = `cookie_consent=${encodeURIComponent(value)};expires=${expires};path=/;SameSite=Lax;Secure`; } function getConsentCookie() { const match = document.cookie.match(/cookie_consent=([^;]+)/); if (!match) return null; try { return JSON.parse(decodeURIComponent(match[1])); } catch { return null; } } ``` #### Restore Previous Consent on Page Load This must run within the 500ms `wait_for_update` window: ```javascript (function restorePreviousConsent() { const saved = getConsentCookie(); if (saved) { gtag('consent', 'update', saved); window.dataLayer.push({ event: 'consent_updated', consent_state: saved }); // Don't show banner — user already chose } else { showConsentBanner(); } })(); ``` #### Consent Action Handlers ```javascript function acceptAll() { const consent = { 'ad_storage': 'granted', 'ad_user_data': 'granted', 'ad_personalization': 'granted', 'analytics_storage': 'granted', 'functionality_storage': 'granted', 'personalization_storage': 'granted' }; gtag('consent', 'update', consent); setConsentCookie(consent); window.dataLayer.push({ event: 'consent_updated', consent_state: consent }); hideConsentBanner(); } function rejectAll() { const consent = { 'ad_storage': 'denied', 'ad_user_data': 'denied', 'ad_personalization': 'denied', 'analytics_storage': 'denied', 'functionality_storage': 'denied', 'personalization_storage': 'denied' }; gtag('consent', 'update', consent); setConsentCookie(consent); window.dataLayer.push({ event: 'consent_updated', consent_state: consent }); hideConsentBanner(); } function saveCustomConsent(choices) { // choices = { analytics: true, marketing: false, functionality: true } const consent = { 'ad_storage': choices.marketing ? 'granted' : 'denied', 'ad_user_data': choices.marketing ? 'granted' : 'denied', 'ad_personalization': choices.marketing ? 'granted' : 'denied', 'analytics_storage': choices.analytics ? 'granted' : 'denied', 'functionality_storage': choices.functionality ? 'granted' : 'denied', 'personalization_storage': choices.functionality ? 'granted' : 'denied' }; gtag('consent', 'update', consent); setConsentCookie(consent); window.dataLayer.push({ event: 'consent_updated', consent_state: consent }); hideConsentBanner(); } ``` #### Critical: The Arguments vs Array Trap When pushing consent commands to `dataLayer`, you MUST use the `gtag()` function which pushes an `Arguments` object. GTM silently ignores regular Arrays for consent commands — this is the most dangerous bug because everything looks correct but GA4 stays permanently in "denied" mode. **Broken** (pushes a regular Array — GTM ignores it silently): ```javascript // DO NOT DO THIS function pushToDataLayer(...args) { window.dataLayer = window.dataLayer || []; window.dataLayer.push(args); // Array, not Arguments — consent is silently ignored } pushToDataLayer('consent', 'update', { analytics_storage: 'granted' }); ``` **Correct** (pushes an Arguments object via `gtag()`): ```javascript // The gtag() function MUST use the `arguments` keyword, not rest params window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('consent', 'update', { analytics_storage: 'granted' }); ``` This bug is especially insidious in TypeScript/React codebases where developers wrap `dataLayer.push` in a helper function using modern syntax (`...args`). The `arguments` keyword is required because GTM checks `instanceof Arguments`, not just the shape of the data. If GA4 shows all hits as cookieless pings (`gcs=G100` in network requests) even after the user accepts cookies, this is almost certainly the cause. #### Banner HTML Structure Build the banner with these requirements: - Fixed position at the bottom or as a modal overlay - Clear visual hierarchy: headline, brief explanation, three buttons (Accept All, Reject All, Customize) - **Equal visual weight for Accept All and Reject All** — same size, same style, same prominence. Do NOT make Accept a filled/primary button and Reject an outline/ghost button. This is a dark pattern under EDPB guidelines and regulators are actively issuing fines for it. Both buttons should use the same variant (both filled, or both outline). - Customization panel with toggles for each category (Analytics, Marketing, Functionality) - "Necessary" category shown but always on and not toggleable - Accessible: proper ARIA labels, keyboard navigation, focus trap when modal is open - No external dependencies — plain HTML/CSS/JS for maximum compatibility - Include a footer link ("Cookie Settings") that re-opens the banner so users can change their choice later ### Step 4: GTM Tag Configuration Inside Google Tag Manager, tags need to respect consent state. **Google tags (GA4, Google Ads, Floodlight):** These have built-in consent checks. When consent mode is active, they automatically: - Fire in "cookieless" mode when storage is denied (sending pings without cookies for modeling) - Fire normally when storage is granted - No additional GTM trigger configuration needed **Non-Google tags (Meta Pixel, LinkedIn Insight, TikTok, etc.):** Create a custom trigger: 1. Create a Custom Event trigger for `consent_updated` 2. Add a condition: check the consent state from the dataLayer 3. Fire the tag only when the relevant consent category is granted For detailed GTM configuration, see [reference/gtm-tag-configuration.md](reference/gtm-tag-configuration.md). ### Step 5: Verify the Implementation Test in this exact order: 1. **Clear all cookies and load the page** — Check browser console: `gtag('consent', 'default', ...)` must fire before GTM loads. In GTM Preview mode, the Consent tab should show all types as "denied." 2. **Click "Reject All"** — GA4 should fire in cookieless/modeling mode (network requests to google-analytics.com but no `_ga` cookie). Marketing tags should not fire at all. 3. **Clear cookies, reload, click "Accept All"** — All tags should fire normally. `_ga` cookie should appear. Marketing pixels should load. 4. **Reload without clearing cookies** — Banner should NOT appear. Consent should restore from cookie within the 500ms window. Tags should fire based on stored consent. 5. **Check GA4 real-time reports** — Hits should appear. If consent mode is working, Google shows a "Consent mode active" indicator in GA4 admin. ## Additional Resources For detailed reference on specific topics: - [Consent Mode v2 parameter reference](reference/consent-mode-defaults.md) — all parameters, default values, and what each controls - [GTM tag configuration guide](reference/gtm-tag-configuration.md) — consent-aware triggers for Google and non-Google tags - [Debugging guide](reference/debugging-guide.md) — common problems and how to diagnose them - [Framework-specific notes](reference/framework-notes.md) — React/Next.js, WordPress, and SPA implementation details - [Consent categories mapping](examples/consent-categories-mapping.md) — how user-facing categories map to Google consent types