DEV Community

Andreas Hatlem
Andreas Hatlem

Posted on

GDPR Cookie Consent Implementation: What Most Developers Get Wrong (and How to Fix It)

Here's a fact that should worry you: roughly 90% of cookie consent implementations on the web are non-compliant with GDPR. Not because developers are lazy — but because the requirements are genuinely misunderstood.

Most sites do this: drop a banner that says "We use cookies" with an "OK" button, load Google Analytics and Meta Pixel on page load regardless of what the user clicks, and call it a day.

That's not consent. Under GDPR, that's a violation that can cost up to 4% of annual global revenue or 20 million euros — whichever is higher. And enforcement is ramping up. In 2024 alone, European Data Protection Authorities issued over 2 billion euros in GDPR fines. Cookie consent violations are among the most commonly reported complaints.

Let's fix this properly.

What GDPR Actually Requires for Cookie Consent

The ePrivacy Directive (often called the "Cookie Law") and GDPR together establish clear rules. Here's what's legally required — not what most banner plugins give you:

1. Prior Consent Before Non-Essential Cookies

No analytics cookies, no marketing pixels, no tracking scripts — nothing non-essential can fire before the user explicitly consents. This is the requirement that most implementations fail.

WRONG:
Page loads -> GA4 fires -> Meta Pixel fires -> Banner appears -> User clicks "OK"
                                                (too late, cookies already set)

RIGHT:
Page loads -> Banner appears -> User clicks "Accept Analytics" -> GA4 fires
                                                (scripts blocked until consent)
Enter fullscreen mode Exit fullscreen mode

This means you need a mechanism to block scripts from executing until consent is given. A banner alone does nothing if your tracking scripts are already in the <head>.

2. Granular Choice

"Accept all or leave" is not valid consent. Users must be able to choose between cookie categories:

  • Necessary/Essential — always allowed, no consent needed (session cookies, CSRF tokens, load balancers)
  • Analytics/Statistics — Google Analytics, Hotjar, Plausible, etc.
  • Marketing/Advertising — Meta Pixel, Google Ads, TikTok Pixel
  • Preferences/Functional — language preferences, theme settings

A single "Accept" button with no alternative except closing the banner does not meet the standard. You need at minimum: Accept All, Reject All, and a way to customize categories.

3. Freely Given (No Dark Patterns)

The "Reject" option must be equally prominent as "Accept." GDPR regulators have specifically called out:

  • Making "Accept" a big green button and "Reject" a tiny text link
  • Requiring multiple clicks to reject but only one to accept
  • Using confusing language ("Legitimate interest" toggles that are on by default)
  • Cookie walls that block content entirely unless you consent

The French DPA (CNIL) fined Google 150 million euros in part because rejecting cookies required multiple clicks while accepting took one.

4. Informed Consent

Before consenting, users must know:

  • What cookies you're setting
  • What each category does
  • Which third parties receive data
  • How long cookies persist

This is why you need a cookie declaration — a detailed list of every cookie, its purpose, provider, and expiry.

5. Documented Proof

If a regulator asks "can you prove this user consented?", you need to show:

  • Timestamp of consent
  • What they consented to (which categories)
  • Version of the consent text they saw
  • Their ability to withdraw at any time

Storing consent only in a browser cookie is not sufficient — it can be deleted. You need server-side consent logging.

6. Easy Withdrawal

Users must be able to change or withdraw consent at any time, as easily as they gave it. This typically means a persistent "Cookie Settings" link in your footer that reopens the consent preferences.

The Technical Implementation

Let's look at how to implement this properly. There are two core challenges: blocking scripts before consent, and communicating consent state to Google and ad platforms.

Blocking Scripts Before Consent

The simplest approach is to change script type attributes so the browser doesn't execute them, then swap them back after consent:

<!-- Instead of this (fires immediately): -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"></script>

<!-- Do this (blocked until consent): -->
<script type="text/plain" data-consent-category="analytics"
        data-src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"></script>
Enter fullscreen mode Exit fullscreen mode

Then in your consent handler:

function enableCategory(category) {
  document
    .querySelectorAll(`script[data-consent-category="${category}"]`)
    .forEach(blockedScript => {
      const newScript = document.createElement('script');

      // Copy all attributes except type and data-consent-category
      for (const attr of blockedScript.attributes) {
        if (attr.name !== 'type' && attr.name !== 'data-consent-category') {
          newScript.setAttribute(
            attr.name === 'data-src' ? 'src' : attr.name,
            attr.value
          );
        }
      }

      // Copy inline script content if any
      if (blockedScript.textContent) {
        newScript.textContent = blockedScript.textContent;
      }

      blockedScript.parentNode.replaceChild(newScript, blockedScript);
    });
}
Enter fullscreen mode Exit fullscreen mode

This works, but it's fragile. You have to manually tag every script, and it doesn't handle dynamically injected scripts (which GTM does constantly). For production use, you need a more robust approach — either Google Tag Manager with built-in consent checks, or a consent management platform that handles script blocking automatically.

Google Consent Mode v2: Why It Matters

Google Consent Mode v2 became mandatory in March 2024 for any site using Google services (Analytics, Ads, etc.) that serves EU users. Without it, Google will stop processing your conversion data from EEA users entirely.

Here's how it works. Instead of blocking Google's scripts completely, you load them but tell Google what the user has consented to. Google's tags then adjust their behavior accordingly:

// Step 1: Set defaults BEFORE loading any Google tags
// This MUST run before gtm.js or gtag.js loads
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }

gtag('consent', 'default', {
  analytics_storage: 'denied',
  ad_storage: 'denied',
  ad_user_data: 'denied',
  ad_personalization: 'denied',
  functionality_storage: 'granted',
  security_storage: 'granted',
  wait_for_update: 500  // Wait 500ms for CMP to load
});
Enter fullscreen mode Exit fullscreen mode

The seven consent parameters in v2 are:

Parameter What It Controls
analytics_storage Google Analytics cookies (_ga, _gid)
ad_storage Advertising cookies (Google Ads, Floodlight)
ad_user_data Whether user data can be sent to Google for ads
ad_personalization Whether ads can be personalized
functionality_storage Functional cookies (language, preferences)
personalization_storage Personalization cookies (video recommendations)
security_storage Security cookies (authentication, fraud prevention)

ad_user_data and ad_personalization are the two new parameters added in v2. They were added to give users more granular control over how their data is used in advertising — and Google requires them for EU traffic.

Step 2: Update Consent When User Chooses

When the user interacts with your consent banner, update the consent state:

function updateGoogleConsent(categories) {
  gtag('consent', 'update', {
    analytics_storage: categories.analytics ? 'granted' : 'denied',
    ad_storage: categories.marketing ? 'granted' : 'denied',
    ad_user_data: categories.marketing ? 'granted' : 'denied',
    ad_personalization: categories.marketing ? 'granted' : 'denied',
    personalization_storage: categories.preferences ? 'granted' : 'denied',
  });
}

// Example: user accepts analytics but rejects marketing
updateGoogleConsent({ analytics: true, marketing: false, preferences: true });
Enter fullscreen mode Exit fullscreen mode

What Happens in Each State

When analytics_storage is denied:

  • Google Analytics still loads and runs
  • It sends "cookieless pings" — anonymized, aggregated data
  • No _ga or _gid cookies are set
  • You get modeled data (Google fills in gaps with machine learning)
  • You keep ~70% of your analytics insight without cookies

When ad_storage is denied:

  • Google Ads tags still load
  • No advertising cookies are set
  • Conversion modeling kicks in (Google estimates conversions)
  • You lose precision but maintain campaign optimization

This is the key insight: Consent Mode doesn't just block or allow — it degrades gracefully. You get some data even when users reject cookies, while staying fully compliant.

Step 3: Handle Returning Users

When a user returns, check their stored consent before setting defaults:

function getStoredConsent() {
  try {
    const stored = localStorage.getItem('cookie_consent');
    if (!stored) return null;

    const data = JSON.parse(stored);

    // Check if consent has expired (re-consent after 12 months per GDPR)
    const maxAge = 365 * 24 * 60 * 60 * 1000;
    if (Date.now() - data.timestamp > maxAge) {
      localStorage.removeItem('cookie_consent');
      return null;
    }

    return data;
  } catch {
    return null;
  }
}

// On page load, before GTM loads:
const existing = getStoredConsent();
if (existing) {
  gtag('consent', 'default', {
    analytics_storage: existing.categories.analytics ? 'granted' : 'denied',
    ad_storage: existing.categories.marketing ? 'granted' : 'denied',
    ad_user_data: existing.categories.marketing ? 'granted' : 'denied',
    ad_personalization: existing.categories.marketing ? 'granted' : 'denied',
    wait_for_update: 500
  });
} else {
  // First visit — deny everything until user chooses
  gtag('consent', 'default', {
    analytics_storage: 'denied',
    ad_storage: 'denied',
    ad_user_data: 'denied',
    ad_personalization: 'denied',
    wait_for_update: 500
  });
}
Enter fullscreen mode Exit fullscreen mode

Region-Specific Defaults

If your site serves both EU and non-EU users, you can set region-specific defaults so non-EU visitors don't see a consent banner at all:

// Strict defaults for EEA countries
gtag('consent', 'default', {
  analytics_storage: 'denied',
  ad_storage: 'denied',
  ad_user_data: 'denied',
  ad_personalization: 'denied',
  region: ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
           'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
           'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'GB', 'NO', 'IS', 'LI', 'CH']
});

// Permissive defaults for everyone else
gtag('consent', 'default', {
  analytics_storage: 'granted',
  ad_storage: 'granted',
  ad_user_data: 'granted',
  ad_personalization: 'granted',
});
Enter fullscreen mode Exit fullscreen mode

This way, US visitors never see a banner and your analytics work normally, while EU visitors get the full consent experience.

Server-Side Consent Logging

Storing consent only in localStorage is not enough. A regulator can ask for proof. You need to log consent server-side with an immutable audit trail:

async function logConsent(consentData) {
  await fetch('/api/consent/log', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      session_id: consentData.id,
      categories: consentData.categories,
      action: consentData.action, // 'accept_all', 'reject_all', 'custom'
      timestamp: new Date().toISOString(),
      banner_version: '2.1',
      page_url: window.location.href,
      user_agent: navigator.userAgent
    })
  });
}
Enter fullscreen mode Exit fullscreen mode

Your backend should store these records for the duration required by your jurisdiction (typically 3-5 years) and make them exportable for audit purposes.

Automated Cookie Scanning

GDPR requires you to declare all cookies your site sets. Manually tracking this is a nightmare — third-party scripts add cookies you don't know about, libraries update, new pixels get added by marketing.

You need automated scanning that periodically crawls your site, detects all cookies and tracking technologies, and flags undeclared ones. This should happen on every deployment, not just once at setup.

If you're building this yourself, you'd need a headless browser (Puppeteer or Playwright) that visits every page, captures all cookies set, and compares them to your declared list. That's a meaningful infrastructure commitment.

Putting It All Together: The Right Way

Here's the complete flow for a properly implemented cookie consent:

1. Page starts loading
2. Consent defaults set (all denied for EU) — BEFORE any tags fire
3. Check for stored consent from previous visit
4. If stored consent exists and not expired:
   a. Update consent state accordingly
   b. Enable permitted scripts
   c. Don't show banner
5. If no stored consent:
   a. Show consent banner
   b. Keep all non-essential scripts blocked
   c. Wait for user interaction
6. User makes a choice (accept all / reject all / customize)
7. Store consent locally AND server-side
8. Update Google Consent Mode state
9. Enable permitted scripts
10. On any subsequent page load, repeat from step 2
Enter fullscreen mode Exit fullscreen mode

The Build vs. Buy Decision

Building all of this yourself is doable, but consider what you're signing up for:

  • Consent banner UI with accessibility (WCAG 2.1) and mobile responsiveness
  • Script blocking and re-enabling engine
  • Google Consent Mode v2 integration
  • IAB TCF 2.2 support (required for programmatic advertising in EU)
  • Automatic cookie scanning and declaration
  • Server-side consent logging with audit trail
  • Geo-detection for region-specific banners
  • Consent expiry and re-consent flows
  • Ongoing maintenance as regulations change

That's a significant project. For most teams, using a purpose-built consent management platform makes more sense.

GetCookies handles all of the above with a single script tag. It blocks non-essential scripts before consent, integrates Google Consent Mode v2 and IAB TCF 2.2 out of the box, includes automated cookie scanning, logs consent server-side with full audit trails, and supports geo-targeted banners for GDPR, CCPA, and ePrivacy. Setup takes about 5 minutes:

<!-- Add to <head> — must load BEFORE Google Tag Manager -->
<script src="https://cdn.getcookies.co/loader.js?id=YOUR_DOMAIN_ID"></script>
Enter fullscreen mode Exit fullscreen mode

The loader script immediately sets Google Consent Mode defaults to denied for all non-essential categories, so no tracking fires before the user consents — even if GTM loads milliseconds later. When the user makes a choice, the widget updates consent state and unblocks the appropriate scripts automatically.

GDPR Cookie Consent Compliance Checklist

Use this to audit your current implementation:

Script Blocking

  • [ ] Non-essential scripts are blocked before consent is given
  • [ ] Scripts only fire after explicit user consent for that category
  • [ ] No cookies are set on first page load (except essential ones)

Consent Banner

  • [ ] Users can accept all, reject all, or customize by category
  • [ ] Reject is equally prominent as Accept (no dark patterns)
  • [ ] Banner clearly explains what each category does
  • [ ] Cookie declaration lists all cookies with purpose, provider, and expiry

Google Consent Mode v2

  • [ ] Consent defaults set BEFORE any Google tags load
  • [ ] All seven parameters are configured (including ad_user_data and ad_personalization)
  • [ ] Consent state updates when user makes a choice
  • [ ] Region-specific defaults for EEA vs. rest of world

Consent Records

  • [ ] Every consent decision is logged server-side
  • [ ] Logs include: timestamp, categories chosen, banner version
  • [ ] Records are retained for required duration (3-5 years)
  • [ ] Records are exportable for audit

User Rights

  • [ ] Users can change preferences at any time (e.g., footer link)
  • [ ] Consent expires and users are re-prompted (max 12 months)
  • [ ] Withdrawal of consent is as easy as giving it

Ongoing

  • [ ] Cookie scanning runs regularly to detect new/changed cookies
  • [ ] Cookie declaration stays up to date
  • [ ] Implementation is tested after every deployment

Common Mistakes to Avoid

1. Loading GTM before setting consent defaults. If gtag('consent', 'default', ...) runs after GTM, tags may fire before consent state is established. The consent default call must be the very first thing in your <head>.

2. Using "legitimate interest" as a catch-all. Legitimate interest is not a valid legal basis for most cookies. Analytics and marketing cookies require consent. Period.

3. Ignoring consent for server-side tracking. Server-side tracking (Meta CAPI, Google Measurement Protocol) doesn't bypass consent requirements. If a user denies marketing cookies, you can't send their data server-side either.

4. Not re-prompting after changes. If you add new cookie categories or change what tracking you use, previous consent may no longer be valid. Users should be re-prompted.

5. Forgetting mobile. Consent banners must be usable on mobile devices. If your banner covers the entire screen with no way to scroll or interact, that's a problem.

Conclusion

GDPR cookie consent is not just about showing a banner. It's about blocking scripts before consent, giving users genuine choice, logging decisions with an audit trail, and integrating properly with Google Consent Mode v2.

Get it wrong, and you're exposed to fines and complaints. Get it right, and it's a solved problem you never have to think about again.

If you want to skip building all of this from scratch, GetCookies gets you to full compliance in about 5 minutes with one script tag. Free trial, no credit card required.

Questions about GDPR implementation? Drop them in the comments — happy to help.

Top comments (0)