Free GDPR-Compliant Cookie Consent for Webflow — No Third-Party Tools

  • webflow
  • gdpr
  • javascript
  • cookie-consent
Free GDPR-Compliant Cookie Consent for Webflow — No Third-Party Tools

Free GDPR-Compliant Cookie Consent for Webflow — No Third-Party Tools

If you’ve ever needed to add a cookie consent modal to a Webflow project, you’ve probably run into the same wall: every solution out there is either a third-party tool, a paid plugin, or a clunky workaround that doesn’t feel native to your project.

I’ve been building Webflow sites at Fri3nds Agency for years, and this kept bothering me. So I built a lightweight, free solution that lets you design your cookie modal entirely in Webflow (the way you’re used to) and handles all the consent logic through a single script.


How it works

The idea is simple: you design your modal in Webflow however you want, assign a few IDs to your elements, and drop the script into your project’s custom code (body). No external dependencies, no monthly fees.

The script handles everything else: storing consent in a cookie (not localStorage, which is the more compliant approach), respecting a 180-day expiration window, showing the modal only when needed, and making sure your tracking scripts only fire after the user explicitly accepts.

There’s also a built-in way to let users reopen the consent modal later, which is a GDPR requirement many implementations forget about.


Setup

You only need four IDs in your Webflow project:

ID Description
cookie-consent Your modal wrapper
cookie-accept The accept button
cookie-reject The reject button
open-cookie-settings Any button or link that should reopen the modal

Then paste your tracking snippets (GTM, Clarity, Amplitude, Hotjar, whatever you use) inside the loadTrackingScripts() function. They’ll only run if the user consents.


The script

<script>
   const CONSENT_KEY = "cookieConsent";
   const CONSENT_DURATION_DAYS = 180;
   const DEBUG = false;
   const log = (...args) => DEBUG && console.log(...args);

   // ─── Cookie Helpers ───────────────────────────────────────────────────────

   function setCookie(name, value, days) {
      const expires = new Date();
      expires.setDate(expires.getDate() + days);
      document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`;
   }

   function getCookie(name) {
      const match = document.cookie
         .split("; ")
         .find((row) => row.startsWith(name + "="));
      return match ? decodeURIComponent(match.split("=")[1]) : null;
   }

   // ─── Consent Logic ────────────────────────────────────────────────────────

   function getConsent() {
      const saved = getCookie(CONSENT_KEY);
      if (!saved) return null;
      try {
         return JSON.parse(saved);
      } catch {
         log("Error parsing consent cookie");
         return null;
      }
   }

   function isExpired(dateString) {
      const diffDays =
         (new Date() - new Date(dateString)) / (1000 * 60 * 60 * 24);
      return diffDays > CONSENT_DURATION_DAYS;
   }

   function handleConsent(accepted) {
      log("User selected:", accepted ? "ACCEPT" : "REJECT");

      const previous = getConsent();
      const previousAccepted = previous ? previous.accepted : null;

      setCookie(
         CONSENT_KEY,
         JSON.stringify({ accepted, date: new Date().toISOString() }),
         CONSENT_DURATION_DAYS,
      );

      const modal = document.getElementById("cookie-consent");
      if (modal) modal.style.display = "none";

      if (accepted) {
         loadTrackingScripts();
      } else {
         if (previousAccepted === true) {
            // The user withdrew consent — reload to stop any scripts already running
            setTimeout(() => location.reload(), 100);
         }
         log("Tracking scripts will NOT be loaded.");
      }
   }

   function bindConsentButtons() {
      const acceptBtn = document.getElementById("cookie-accept");
      const rejectBtn = document.getElementById("cookie-reject");

      if (acceptBtn) acceptBtn.onclick = () => handleConsent(true);
      if (rejectBtn) rejectBtn.onclick = () => handleConsent(false);
   }

   // ─── Tracking Scripts ─────────────────────────────────────────────────────
   // Paste your tracking snippets inside this function.
   // You can add as many as you need (GTM, Clarity, Amplitude, Hotjar, etc.)
   // These will only run if the user has accepted cookies.

   function loadTrackingScripts() {
      log("Loading tracking scripts...");

      // ↓ Paste your snippets here ↓

      // ↑ End of tracking scripts ↑
   }

   // ─── Init ─────────────────────────────────────────────────────────────────

   document.addEventListener("DOMContentLoaded", () => {
      const modal = document.getElementById("cookie-consent");
      const consent = getConsent();

      // Allows the user to reopen the cookie modal from anywhere on the site.
      // Add the id "open-cookie-settings" to any button or link.
      const settingsBtn = document.getElementById("open-cookie-settings");
      if (settingsBtn) {
         settingsBtn.addEventListener("click", (e) => {
            e.preventDefault();
            if (modal) modal.style.display = "block";
            bindConsentButtons();
         });
      }

      if (consent && !isExpired(consent.date)) {
         if (consent.accepted) loadTrackingScripts();
         if (modal) modal.style.display = "none";
      } else {
         // No valid consent found — show the modal
         if (modal) modal.style.display = "block";
         bindConsentButtons();
      }
   });
</script>

A few things worth knowing

The script uses cookies instead of localStorage to store consent. This is intentional — cookies have native expiration handling at the browser level, which is the more accepted approach in compliance contexts.

If a user who previously accepted decides to withdraw consent, the page reloads automatically to stop any tracking scripts that were already running. The reverse (going from reject to accept) just loads the scripts on the spot without a reload, since nothing was running anyway.

And if you want to debug locally, just flip DEBUG to true at the top of the script and you’ll get console logs for every step of the flow.

That’s it. No setup wizards, no cookie banners SDK, no npm install. Just paste, assign your IDs, and you’re done.