Back to Blog & Guides/GDPR Cookie Consent Banner & Google Consent Mode v2: Technical Implementation Guide
#GDPR#Frontend#JavaScript#Consent Mode v2#Cookies#Privacy

GDPR Cookie Consent Banner & Google Consent Mode v2: Technical Implementation Guide

A comprehensive developer guide to building a zero-telemetry, GDPR & ePrivacy compliant cookie consent manager supporting Google Consent Mode v2, granular preference categories, script blocking, and automated consent testing.

SovereignShield Web Security

Building a compliant web application in the European Economic Area (EEA) and the United Kingdom requires adhering strictly to two overlapping privacy frameworks mapped in our Interactive GDPR Compliance Matrix:

  1. The ePrivacy Directive (2002/58/EC as amended by 2009/136/EC): Specifically Article 5(3), which mandates prior, informed user consent before storing or accessing information on a user’s terminal equipment (cookies, localStorage, IndexedDB, canvas fingerprinting).
  2. The General Data Protection Regulation (EU 2016/679 - GDPR): Specifically Article 4(11) defining valid consent, Article 7 governing conditions for consent, and European Data Protection Board (EDPB) Guidelines 05/2020. You can also generate turnkey disclosures using our Public Trust Center Compiler.

The CJEU Planet49 Landmark Ruling (Case C-673/17)

The Court of Justice of the European Union (CJEU) established key legal precedents for online tracking:

  • No Pre-Ticked Checkboxes: Pre-selected option boxes do not constitute active, valid consent.
  • Strict Opt-in Requirement: Non-essential tracking cookies (Analytics, Advertising, Personalization) must remain completely blocked until the user takes affirmative action.
  • Equal Prominence: Rejecting consent must be as effortless as accepting consent. “Reject All” buttons must be positioned alongside “Accept All” with identical visual weight, font sizing, and button contrast. Dark patterns (such as hiding the reject option inside sub-menus or using low-contrast text) violate Article 7(3).

Beginning March 2024, Google mandated Consent Mode v2 for all applications using Google Ads, Google Analytics 4 (GA4), or Floodlight tags in the EEA/UK. Consent Mode v2 introduces two mandatory parameter signals alongside legacy flags:

  • ad_storage: Enables storage (such as cookies) related to advertising.
  • analytics_storage: Enables storage (such as cookies) related to analytics (e.g. visit duration).
  • ad_user_data: Controls whether user data can be sent to Google for online advertising purposes.
  • ad_personalization: Controls whether personalized advertising (remarketing) can be enabled.

To prevent race conditions where tracking scripts execute before default consent states are declared, the initialization snippet must be placed as the very first script block in the HTML <head>, preceding Google Tag Manager (GTM), GA4, or any ad scripts.

<!-- HTML Head Initialization - Positioned ABOVE GTM/GA4 Scripts -->
<script>
  // Initialize dataLayer array
  window.dataLayer = window.dataLayer || [];
  function gtag(){ dataLayer.push(arguments); }

  // Set DEFAULT consent states to 'denied' BEFORE loading analytics or marketing tags
  gtag('consent', 'default', {
    'ad_storage': 'denied',
    'analytics_storage': 'denied',
    'ad_user_data': 'denied',
    'ad_personalization': 'denied',
    'functionality_storage': 'granted', // Essential cookies for core site functionality
    'security_storage': 'granted',      // Essential security/CSRF tokens
    'wait_for_update': 500              // Wait up to 500ms for saved consent retrieval
  });

  // Enable advanced redaction for ad signals when ad_storage is denied
  gtag('set', 'ads_data_redaction', true);
  gtag('set', 'url_passthrough', true);
</script>

Below is a complete, modular, framework-agnostic JavaScript implementation (CookieConsentManager) that handles local preference persistence, granular modal controls, dynamic script injection, and signal synchronization.

/**
 * SovereignShield Cookie & Privacy Consent Manager v2
 * Zero-telemetry, local-first client-side implementation.
 */
export class CookieConsentManager {
  constructor(options = {}) {
    this.STORAGE_KEY = options.storageKey || 'sovereign_consent_state_v2';
    this.bannerId = 'sovereign-gdpr-banner';
    this.modalId = 'sovereign-gdpr-modal';
    this.callbacks = options.onConsentUpdate || null;

    this.defaultState = {
      essential: true,
      analytics: false,
      marketing: false,
      functional: false,
      timestamp: null,
      version: '2.0.0'
    };

    this.init();
  }

  init() {
    const savedConsent = this.getSavedConsent();
    if (savedConsent) {
      this.applyConsent(savedConsent, false);
    } else {
      this.renderBanner();
    }
    this.attachFooterListeners();
  }

  getSavedConsent() {
    try {
      const data = localStorage.getItem(this.STORAGE_KEY);
      return data ? JSON.parse(data) : null;
    } catch (err) {
      console.warn('[ConsentManager] Unable to access localStorage:', err);
      return null;
    }
  }

  saveConsent(state) {
    const consentPayload = {
      ...state,
      essential: true,
      timestamp: new Date().toISOString(),
      version: '2.0.0'
    };

    try {
      localStorage.setItem(this.STORAGE_KEY, JSON.stringify(consentPayload));
    } catch (err) {
      console.error('[ConsentManager] Failed to persist consent state:', err);
    }

    this.applyConsent(consentPayload, true);
    this.closeBanner();
    this.closeModal();
  }

  applyConsent(state, triggerEvents = true) {
    // Synchronize with Google Consent Mode v2 API
    if (typeof window.gtag === 'function') {
      window.gtag('consent', 'update', {
        'analytics_storage': state.analytics ? 'granted' : 'denied',
        'ad_storage': state.marketing ? 'granted' : 'denied',
        'ad_user_data': state.marketing ? 'granted' : 'denied',
        'ad_personalization': state.marketing ? 'granted' : 'denied',
        'functionality_storage': state.functional ? 'granted' : 'denied'
      });
    }

    // Trigger dynamic script execution for granted categories
    if (state.analytics) this.loadAnalyticsScripts();
    if (state.marketing) this.loadMarketingScripts();

    // Dispatch Custom DOM Event for internal components
    window.dispatchEvent(new CustomEvent('sovereignConsentChanged', { detail: state }));

    if (triggerEvents && typeof this.callbacks === 'function') {
      this.callbacks(state);
    }
  }

  loadAnalyticsScripts() {
    if (window.analyticsScriptsLoaded) return;
    window.analyticsScriptsLoaded = true;

    // Dynamically inject GA4 or PostHog scripts only after consent is confirmed
    const script = document.createElement('script');
    script.async = true;
    script.src = 'https://www.googletagmanager.com/gtag/js?id=G-YOURTRACKINGID';
    document.head.appendChild(script);
  }

  loadMarketingScripts() {
    if (window.marketingScriptsLoaded) return;
    window.marketingScriptsLoaded = true;

    // Inject Marketing Pixel Scripts
    console.log('[ConsentManager] Marketing consent granted. Initializing ad pixels.');
  }

  renderBanner() {
    if (document.getElementById(this.bannerId)) return;

    const bannerHtml = `
      <div id="${this.bannerId}" class="consent-banner-wrapper" role="region" aria-label="Cookie Consent Banner">
        <div class="consent-banner-content">
          <div class="consent-text">
            <h3>Cookie & Privacy Preferences</h3>
            <p>
              We enforce strict GDPR Article 7 compliance. Non-essential cookies and analytics tools are disabled by default. 
              You can choose to accept all, reject non-essential cookies, or customize your granular preferences. 
              Read our <a href="/privacy/">Privacy Policy</a> and <a href="/terms/">Terms of Service</a> for details.
            </p>
          </div>
          <div class="consent-actions">
            <button id="gdpr-btn-accept-all" class="btn-primary">Accept All</button>
            <button id="gdpr-btn-reject-all" class="btn-secondary">Reject Non-Essential</button>
            <button id="gdpr-btn-customize" class="btn-outline">Customize Preferences</button>
          </div>
        </div>
      </div>
    `;

    document.body.insertAdjacentHTML('beforeend', bannerHtml);
    this.attachBannerEvents();
  }

  attachBannerEvents() {
    const banner = document.getElementById(this.bannerId);
    if (!banner) return;

    banner.querySelector('#gdpr-btn-accept-all').addEventListener('click', () => {
      this.saveConsent({ analytics: true, marketing: true, functional: true });
    });

    banner.querySelector('#gdpr-btn-reject-all').addEventListener('click', () => {
      this.saveConsent({ analytics: false, marketing: false, functional: false });
    });

    banner.querySelector('#gdpr-btn-customize').addEventListener('click', () => {
      this.renderModal();
    });
  }

  renderModal() {
    if (document.getElementById(this.modalId)) return;

    const currentConsent = this.getSavedConsent() || this.defaultState;

    const modalHtml = `
      <div id="${this.modalId}" class="consent-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-title">
        <div class="consent-modal-card">
          <div class="consent-modal-header">
            <h2 id="modal-title">Privacy & Cookie Settings</h2>
            <button id="modal-close-btn" class="close-x" aria-label="Close Modal">&times;</button>
          </div>

          <div class="consent-modal-body">
            <!-- Category 1: Essential -->
            <div class="consent-category">
              <div class="category-header">
                <div>
                  <strong>Strictly Necessary Cookies</strong>
                  <span class="badge-required">Always Active</span>
                </div>
              </div>
              <p>Required for security authentication, CSRF protection, and session integrity. Cannot be disabled.</p>
            </div>

            <!-- Category 2: Analytics -->
            <div class="consent-category">
              <div class="category-header">
                <label for="toggle-analytics">
                  <strong>Analytics & Performance Cookies</strong>
                </label>
                <input type="checkbox" id="toggle-analytics" ${currentConsent.analytics ? 'checked' : ''}>
              </div>
              <p>Helps us measure site traffic, page response times, and feature interaction patterns anonymously.</p>
            </div>

            <!-- Category 3: Marketing -->
            <div class="consent-category">
              <div class="category-header">
                <label for="toggle-marketing">
                  <strong>Marketing & Targeting Cookies</strong>
                </label>
                <input type="checkbox" id="toggle-marketing" ${currentConsent.marketing ? 'checked' : ''}>
              </div>
              <p>Used to deliver personalized ad content, campaign attribution, and Google Consent Mode v2 ad signals.</p>
            </div>
          </div>

          <div class="consent-modal-footer">
            <button id="modal-save-preferences" class="btn-primary">Save Selected Preferences</button>
          </div>
        </div>
      </div>
    `;

    document.body.insertAdjacentHTML('beforeend', modalHtml);

    const modal = document.getElementById(this.modalId);
    modal.querySelector('#modal-close-btn').addEventListener('click', () => this.closeModal());
    modal.querySelector('#modal-save-preferences').addEventListener('click', () => {
      const analytics = modal.querySelector('#toggle-analytics').checked;
      const marketing = modal.querySelector('#toggle-marketing').checked;
      this.saveConsent({ analytics, marketing, functional: false });
    });
  }

  closeBanner() {
    const banner = document.getElementById(this.bannerId);
    if (banner) banner.remove();
  }

  closeModal() {
    const modal = document.getElementById(this.modalId);
    if (modal) modal.remove();
  }

  attachFooterListeners() {
    // Re-open preferences panel when user clicks footer 'Cookie Preferences' link
    document.querySelectorAll('.trigger-cookie-preferences').forEach(el => {
      el.addEventListener('click', (e) => {
        e.preventDefault();
        this.renderModal();
      });
    });
  }
}

4. CSS Design Rules for Equal Prominence & Accessibility

To pass manual compliance checks by data protection authorities and Google AdSense reviewers, the banner must strictly adhere to WCAG 2.1 AA accessibility guidelines and CSS visual balance rules.

/* SovereignShield Cookie Banner Design System */
.consent-banner-wrapper {
  position: fixed;
  bottom: 1.5rem;
  left: 1.5rem;
  right: 1.5rem;
  max-width: 48rem;
  margin: 0 auto;
  background-color: #09090b;
  border: 1px solid #27272a;
  border-radius: 1.25rem;
  padding: 1.5rem;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.75);
  z-index: 99999;
  color: #f4f4f5;
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

.consent-banner-content h3 {
  font-size: 1.125rem;
  font-weight: 800;
  color: #10b981;
  margin-bottom: 0.5rem;
}

.consent-banner-content p {
  font-size: 0.875rem;
  color: #a1a1aa;
  line-height: 1.6;
  margin-bottom: 1.25rem;
}

.consent-banner-content a {
  color: #34d399;
  text-decoration: underline;
}

.consent-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

/* Equal Prominence Buttons */
.consent-actions button {
  flex: 1 1 140px;
  padding: 0.75rem 1rem;
  font-size: 0.875rem;
  font-weight: 700;
  border-radius: 0.75rem;
  cursor: pointer;
  transition: all 0.2s ease;
  border: none;
  text-align: center;
}

.btn-primary {
  background-color: #10b981;
  color: #000000;
}
.btn-primary:hover {
  background-color: #34d399;
}

.btn-secondary {
  background-color: #27272a;
  color: #ffffff;
  border: 1px solid #3f3f46 !important;
}
.btn-secondary:hover {
  background-color: #3f3f46;
}

.btn-outline {
  background-color: transparent;
  color: #a1a1aa;
  border: 1px solid #27272a !important;
}
.btn-outline:hover {
  color: #ffffff;
  border-color: #52525b !important;
}

5. Technical Safeguards & Verification Matrix

The matrix below maps frontend consent architecture features to specific GDPR and ePrivacy articles:

Feature Target GDPR / ePrivacy Reference Technical Implementation Verification Method
Default Blocked State ePrivacy Art. 5(3) & GDPR Art. 7(1) gtag('consent', 'default', { ... }) set to denied Audit Network Tab before interaction
Equal Prominence EDPB Guidelines 05/2020 Reject & Accept buttons share equal CSS sizing Visual contrast audit (WCAG AA)
Granular Controls GDPR Art. 6(1)(a) Separate toggles for Analytics, Marketing, Functional Check toggle states in Modal DOM
Local Persistence GDPR Art. 25 (Privacy by Design) Sandboxed localStorage persistence with ISO timestamps Inspect localStorage.getItem('sovereign_consent_state_v2')
Revocation Support GDPR Art. 7(3) Footer .trigger-cookie-preferences link re-opens modal Trigger click & verify state update

6. Automated End-to-End Testing with Playwright

To prevent regressions in compliance behavior during continuous integration (CI/CD) deployments, use the automated Playwright test script below:

// tests/e2e/cookie-consent.spec.ts
import { test, expect } from '@playwright/test';

test.describe('GDPR Cookie Consent Engine Verification', () => {
  test.beforeEach(async ({ page }) => {
    // Clear storage before test
    await page.goto('http://localhost:4321/');
    await page.evaluate(() => localStorage.clear());
    await page.reload();
  });

  test('should display banner on first visit with default consent denied', async ({ page }) => {
    const banner = page.locator('#sovereign-gdpr-banner');
    await expect(banner).toBeVisible();

    // Verify Google Consent Mode default object in window
    const defaultConsent = await page.evaluate(() => {
      return (window as any).dataLayer?.find((entry: any) => entry[0] === 'consent' && entry[1] === 'default');
    });

    expect(defaultConsent[2].analytics_storage).toBe('denied');
    expect(defaultConsent[2].ad_storage).toBe('denied');
  });

  test('should persist rejection when "Reject Non-Essential" is clicked', async ({ page }) => {
    await page.click('#gdpr-btn-reject-all');
    await expect(page.locator('#sovereign-gdpr-banner')).not.toBeVisible();

    const storedData = await page.evaluate(() => {
      return JSON.parse(localStorage.getItem('sovereign_consent_state_v2') || '{}');
    });

    expect(storedData.analytics).toBe(false);
    expect(storedData.marketing).toBe(false);
    expect(storedData.essential).toBe(true);
  });
});

Conclusion & Action Plan

Deploying a zero-telemetry, GDPR-compliant cookie consent system requires more than rendering a banner overlay. Developers must guarantee that script tags remain completely suppressed prior to explicit user opt-in, synchronize consent signals with Google Consent Mode v2, and enable ongoing user revocation via permanent footer links.

To continue hardening your data privacy stack across related regulatory frameworks:

CONTINUE READING

Related Compliance Engineering Guides

Explore SovereignShield Compliance Automation Tools

Audit GDPR & HIPAA controls, generate public trust centers, and export certified PDF proofs with zero data egress.