Creating a Cookie Consent Banner for Google Tag Manager

Guide on creating a cookie consent banner to require consent before enabling Google Tag Mangager and GA4, using Astro and SolidJS.

Cover image for Creating a Cookie Consent Banner for Google Tag Manager
Web Reaper avatar
Web Reaper

10 min read


What We Will Make

We will create a basic cookie consent banner that will require users to accept before enabling Google Tag Manager and GA4. This will be done using Astro, SolidJS, and Tailwind CSS, although you can use it with whatever framework you want. We are going to setup the code to have GA4 automatically deny all consent, then call a function to update GA4 with the consent when it is accepted by the user. This can be tracked in a cookie in the user’s browser. The banner will look like the below.

We use cookies to analyze our website and make your experience even better. To learn more, see our Privacy Policy.

If you have EU users, you need to get their permission before you store their personal data. This includes cookies for GTM / GA4. You can read more about this at GDPR.eu. This is why you see cookie consent banners on many websites. Let’s get the annoying disclaimer out of the way - I am not a lawyer and none of this is legal advice. You should consult a lawyer to make sure you are compliant with the law.

We’re going to create a cookie consent banner component, I call it CookieConsent.tsx. This will display a banner if the user has not yet consented to cookies. If they have consented, it will not display anything. The banner will have a button to accept cookies, and a link to a privacy policy. The banner will be fixed to the bottom-right of the screen.

Prerequisites

While this can be adapted to any framework, I will be using Astro, SolidJS, and Tailwind CSS.

TIP

SolidJS and React are extremely similar and it is easy to switch this component to React. I will probably update the post at some point to also include the React version.

You will need:

This is a follow-on to a previous blog post of mine, on setting up GA4 and Google Tag Manager on your Astro website. If you have not already setup GA4 and GTM, you should go to that blog post first and set it up.

Starwind UI

starwind ui

I've learned a lot developing with Astro. So I crafted 37+ animated and accessible components to help you get started with your next project. Inspired by shadcn/ui with seamless CLI installation. Free and open source.

Head Setup

We need to update the <head> of our site to default deny cookies, and create a function to update when cookies are accepted. These will be global functions so they can be accessed within our component. We will also create a function to get the value of the cookie.

<!-- Google tag (gtag.js) -->
<script is:inline>
  window.dataLayer = window.dataLayer || [];
  function gtag() {
    dataLayer.push(arguments);
  }

  // function to update GTM that consent is granted
  // You can set each type of cookie separately here
  window.consentGranted = function () {
    console.log("consent granted");
    gtag("consent", "update", {
      ad_storage: "granted",
      analytics_storage: "granted",
      functionality_storage: "granted",
      personalization_storage: "granted",
      security_storage: "granted",
    });
  };

  // checks the "cookie-consent" cookie
  window.getCookieConsent = function () {
    try {
      const consent = document.cookie.match(/cookie-consent=([^;]+)/)[1];
      return consent;
    } catch (error) {
      return "unk";
    }
  };

  if (window.getCookieConsent() == "granted") {
    // If cookies already approved, set that
    console.log("cookies already approved");
    gtag("consent", "default", {
      ad_storage: "granted",
      analytics_storage: "granted",
      functionality_storage: "granted",
      personalization_storage: "granted",
      security_storage: "granted",
    });
  } else {
    // default all to 'denied'.
    console.log("default set to denied");
    gtag("consent", "default", {
      ad_storage: "denied",
      analytics_storage: "denied",
      functionality_storage: "denied",
      personalization_storage: "denied",
      security_storage: "denied",
    });
  }
</script>

Component Structure

Create CookieConsent.tsx in your src/components folder. We’re going to use SolidJS to create this component. Some notes:

  • Show allows us to show the component only when a condition is met. In this case, we only want to show the component if the user has not yet consented to cookies.
  • We will use the isMounted signal to make sure the component is mounted before we check the cookie. This is to prevent a component flash.
import { Component, createSignal, createEffect, Show } from "solid-js";

// tell typescript that this function is defined in the global scope
// they are defined in the <head> per the previous instructions
declare function consentGranted(): void;
declare function getCookieConsent(): string;

const CookieConsent: Component = () => {
  const [cookies, setCookies] = createSignal("unk");
  const [isMounted, setIsMounted] = createSignal(false);

  const handleAccept = () => {
    setCookies("granted");
    // accepted cookie only lasts for the session
    document.cookie = "cookie-consent=granted; path=/";
    // call global function (head defined) to update GA4
    consentGranted();
  };

  const handleDecline = () => {
    setCookies("denied");
    // declined cookie only lasts for the session
    document.cookie = "cookie-consent=denied; path=/";
  };

  // this waits to load the cookie banner until the component is mounted
  // so that there is not a component flash
  createEffect(() => {
    setIsMounted(true);
    // get cookie approval after component is mounted
    setCookies(getCookieConsent());
  });

  // if there is no cookie for "cookie-consent", display the banner
  return (
    <Show when={isMounted()} fallback={null}>
      <div
        id="cookie-banner"
        class={`${
          cookies() === "granted" || cookies() === "denied" ? "hidden" : ""
        } fixed bottom-0 right-0 z-50 m-2 max-w-screen-sm rounded-lg border-2 border-slate-300 bg-purple-50 text-slate-800 shadow-xl`}
      >
        <div class="p-4 text-center">
          <p class="mb-4 text-sm sm:text-base">
            We use cookies to analyze our website and make your experience even
            better. To learn more, see our{" "}
            <a
              class="text-blue-600 underline hover:text-blue-700"
              href="/privacy-policy"
              target="_blank"
            >
              Privacy Policy.
            </a>
          </p>

          <div class="mx-auto">
            <button
              class="rounded-md bg-blue-600 p-2 text-white transition hover:bg-blue-700"
              onClick={handleAccept}
            >
              Accept
            </button>
            <button
              class="ml-2 rounded-md bg-transparent p-2 text-slate-600 transition hover:bg-gray-200"
              onClick={handleDecline}
            >
              Decline
            </button>
          </div>
        </div>
      </div>
    </Show>
  );
};

export default CookieConsent;

Make It Last Longer

The cookie we set only lasts for the session. This means that when the user’s browser is closed, the cookie banner will appear again. Users don’t like to be bothered by the banners, so you can make it last for a longer period.

INFO

Some browsers will limit the maximum duration of cookie storage.

Let’s make it last for a year if the user accepts. We will update the handleAccept function to set the cookie to expire in a year.

const handleAccept = () => {
  setCookies("granted");
  // accepted cookie lasts for a year
  let d = new Date();
  let oneYear = new Date(d.getFullYear() + 1, d.getMonth(), d.getDate());
  document.cookie = "cookie-consent=granted; expires=" + oneYear + "; path=/";
  consentGranted();
};

Final Code

Head Code

INFO

I use is:inline to make sure the script is inlined in the HTML. This is because I use Astro and it needs to be inlined to work properly. If you don’t use Astro, you can probably remove the is:inline and just have the script tags.

In your <head> you need:

<head>
  <!-- Google tag (gtag.js) -->
  <script is:inline>
    window.dataLayer = window.dataLayer || [];
    function gtag() {
      dataLayer.push(arguments);
    }

    // global function so that it can be called from anywhere
    // Updates consent for GA4 to granted if called
    window.consentGranted = function () {
      console.log("consent granted");
      gtag("consent", "update", {
        ad_storage: "granted",
        analytics_storage: "granted",
        functionality_storage: "granted",
        personalization_storage: "granted",
        security_storage: "granted",
      });
    };

    // Returns value of a cookie named 'cookie-consent'
    // Should be either "granted" or "denied"
    window.getCookieConsent = function () {
      try {
        const consent = document.cookie.match(/cookie-consent=([^;]+)/)[1];
        return consent;
      } catch (error) {
        return "unk";
      }
    };

    if (window.getCookieConsent() == "granted") {
      // If cookies already approved, set that
      console.log("default set to granted");
      gtag("consent", "default", {
        ad_storage: "granted",
        analytics_storage: "granted",
        functionality_storage: "granted",
        personalization_storage: "granted",
        security_storage: "granted",
      });
    } else {
      // default all to 'denied'.
      console.log("default set to denied");
      gtag("consent", "default", {
        ad_storage: "denied",
        analytics_storage: "denied",
        functionality_storage: "denied",
        personalization_storage: "denied",
        security_storage: "denied",
      });
    }
  </script>

  <!-- Google Tag Manager -->
  <script is:inline>
    (function (w, d, s, l, i) {
      console.log("GTM");
      w[l] = w[l] || [];
      w[l].push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
      var f = d.getElementsByTagName(s)[0],
        j = d.createElement(s),
        dl = l != "dataLayer" ? "&l=" + l : "";
      j.async = true;
      j.src = "https://www.googletagmanager.com/gtm.js?id=" + i + dl;
      f.parentNode.insertBefore(j, f);
    })(window, document, "script", "dataLayer", "GTM-XXXXXXX");
  </script>
  <!-- End Google Tag Manager -->
</head>

Body Code

In the <body> you need:

<!-- This is opening of <body> -->
<!-- Google Tag Manager (noscript) -->
<noscript>
  <iframe
    src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
    height="0"
    width="0"
    style="display:none;visibility:hidden"
  >
  </iframe>
</noscript>
<!-- End Google Tag Manager (noscript) -->

SolidJS Component

CookieConsent.tsx

import { Component, createSignal, createEffect, Show } from "solid-js";

// tell typescript that this function is defined in the global scope
declare function consentGranted(): void;
declare function getCookieConsent(): string;

const CookieConsent: Component = () => {
  const [cookies, setCookies] = createSignal("unk");
  const [isMounted, setIsMounted] = createSignal(false);

  // get dates for cookie expiration
  let d = new Date();
  let oneYear = new Date(d.getFullYear() + 1, d.getMonth(), d.getDate());

  const handleAccept = () => {
    setCookies("granted");
    // accepted cookie lasts for a year
    document.cookie = "cookie-consent=granted; expires=" + oneYear + "; path=/";
    // call global function (head defined) to update GA4
    consentGranted();
  };

  const handleDecline = () => {
    setCookies("denied");
    // declined cookie only lasts for the session
    // if you don't want to be evil you can set it to a year
    document.cookie = "cookie-consent=denied; path=/";
  };

  // this waits to load the cookie banner until the component is mounted
  // so that there is not a component flash
  createEffect(() => {
    setIsMounted(true);
    // get cookie approval after component is mounted
    setCookies(getCookieConsent());
  });

  // if there is no cookie for "cookie-consent", display the banner
  return (
    <Show when={isMounted()} fallback={null}>
      <div
        id="cookie-banner"
        class={`${
          cookies() === "granted" || cookies() === "denied" ? "hidden" : ""
        } fixed bottom-0 right-0 z-50 m-2 max-w-screen-sm rounded-lg border-2 border-slate-300 bg-purple-50 text-slate-800 shadow-xl`}
      >
        <div class="p-4 text-center">
          <p class="mb-4 text-sm sm:text-base">
            We use cookies to analyze our website and make your experience even
            better. To learn more, see our{" "}
            <a
              class="text-blue-600 underline hover:text-blue-700"
              href="/privacy-policy"
            >
              Privacy Policy.
            </a>
          </p>

          <div class="mx-auto">
            <button
              class="rounded-md bg-blue-600 p-2 text-white transition hover:bg-blue-700"
              onClick={handleAccept}
            >
              Accept
            </button>
            <button
              class="ml-2 rounded-md bg-transparent p-2 text-slate-600 transition hover:bg-gray-200"
              onClick={handleDecline}
            >
              Decline
            </button>
          </div>
        </div>
      </div>
    </Show>
  );
};

export default CookieConsent;

Starwind UI

starwind ui

I've learned a lot developing with Astro. So I crafted 37+ animated and accessible components to help you get started with your next project. Inspired by shadcn/ui with seamless CLI installation. Free and open source.

Test Your GA4 Implementation

You can test your GA4 implementation at tagassistant.google.com. This will tell you if you have any errors in your implementation. With no cookies set, you should see that GA4 is not in use until you hit accept. Notes:

  • Make sure your browser is not blocking cookies
  • Make sure you are not using an ad blocker
  • You can reset your cookies in the browser dev tools under “Application” -> “Cookies”

Things To Keep In Mind

To use the code in Astro, you need to import the component into your .astro file. You can then use it like any other component from Solid, React, etc. Ex. <CookieConsent client:load />.

You’ll likely want GTM and the Cookie Consent Banner to run on every page, so it is recommended to include it in a layout component that is utilized across all of your pages.

Great Success

You now have a cookie consent banner that will require users to accept before enabling Google Tag Manager and GA4. Great success. 🚀

Don't forget to follow me on Twitter for more Astro, web development, and web design tips!


Share this post!