Setting Up Tag Management and Consent with GTM, Klaro, and GA4

Introduction

Most teams start tracking in code because it feels straightforward at first. Then the first real campaign cycle hits, requirements change weekly, and every small update suddenly depends on engineering capacity.

That is the problem this setup solves.

I keep the tracking and consent control plane in Google Tag Manager, use Klaro as the consent UI and source of truth, and map everything into consent mode so behavior is explicit and debuggable.

The result is a cleaner operating model:

  • marketing can move faster without waiting for deployments,
  • engineering is not blocked by day-to-day tag edits,
  • and both sides can verify exactly what fired and why.

This guide is the exact implementation flow I use.

The Starting Point: Why Not Hardcode It?

Hardcoding analytics and consent behavior in templates creates long feedback loops.

A small change in copy, trigger logic, or consent behavior often means:

  • opening a ticket,
  • waiting for prioritization,
  • shipping code,
  • and re-testing everything in production.

For fast-moving teams, that process creates avoidable friction. GTM is not only about convenience. It is about operational clarity and speed across teams.

Who This Is For

This guide is for founders, marketers, and developers who want a consent-aware setup that is easy to operate without constant engineering support.

Prerequisites

  • Access to your Google Tag Manager container
  • Access to website templates/layouts to place GTM snippets
  • Klaro script access via CDN
  • A published privacy policy page (used in Klaro translations)

Architecture Overview

High-level flow:

  1. Klaro is loaded via GTM as early as possible.
  2. Consent choices are stored in the Klaro cookie.
  3. GTM variables read that cookie and convert values to consent mode signals.
  4. Consent Mode updates what can fire (analytics, ads, personalization, etc.).
  5. Custom consent events update tag behavior without forcing a page reload.

Phase 1: Load Klaro First and Load It Reliably

Create a GTM Custom HTML tag that loads Klaro.

  • Tag name: Klaro - Consent Banner
  • Tag type: Custom HTML
  • Trigger: Consent Initialization - All Pages (only this trigger)

Use this stripped setup (Google Analytics only), with styling aligned to the website design tokens:

<link rel="stylesheet" href="https://cdn.kiprotect.com/klaro/v0.7.18/klaro.min.css" />

<style>
  #klaro .cookie-notice {
    background: var(--card, #ffffff);
    color: var(--foreground, #1a1a1a);
    border: 1px solid var(--border, #d1d1c7);
    border-radius: calc(var(--radius, 0.5rem) + 0.25rem);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
    padding: 1rem;
    max-width: 380px;
  }

  #klaro .cookie-modal .cm-modal {
    background: var(--card, #ffffff);
    color: var(--foreground, #1a1a1a);
    border: 1px solid var(--border, #d1d1c7);
    border-radius: calc(var(--radius, 0.5rem) + 0.25rem);
    box-shadow: 0 18px 45px rgba(0, 0, 0, 0.18);
  }

  #klaro .cookie-notice p,
  #klaro .cookie-notice strong,
  #klaro .cookie-notice span,
  #klaro .cookie-modal p,
  #klaro .cookie-modal strong,
  #klaro .cookie-modal span {
    color: var(--foreground, #1a1a1a) !important;
  }

  #klaro h3 {
    margin: 0 0 0.75rem;
    font-size: 1rem;
    line-height: 1.4;
    font-weight: 600;
  }

  #klaro p {
    font-size: 0.95rem;
    line-height: 1.5;
    margin: 0;
  }

  .klaro .cookie-modal a,
  .klaro .context-notice a,
  .klaro .cookie-notice a,
  .cm-link {
    color: var(--primary, #d97757) !important;
  }

  .cm-btn {
    border-radius: var(--radius, 0.5rem) !important;
    padding: 0.6rem 1rem !important;
    font-weight: 600 !important;
    border: 1px solid var(--border, #d1d1c7) !important;
  }

  .cm-btn-success {
    background: var(--primary, #d97757) !important;
    border-color: var(--primary, #d97757) !important;
    color: var(--primary-foreground, #ffffff) !important;
  }

  .cm-btn-danger {
    background: var(--card, #ffffff) !important;
    color: var(--foreground, #1a1a1a) !important;
  }

  .cm-link.cn-learn-more {
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    width: fit-content !important;
    min-width: 0 !important;
    white-space: nowrap !important;
    background: var(--secondary, #e6e4dd) !important;
    border: 1px solid var(--border, #d1d1c7) !important;
    border-radius: var(--radius, 0.5rem) !important;
    padding: 0.6rem 1rem !important;
    color: var(--foreground, #1a1a1a) !important;
  }

  #klaro .cookie-notice .cn-buttons {
    display: flex !important;
    flex-wrap: wrap !important;
    gap: 0.5rem !important;
  }

  #klaro .cookie-notice .cn-buttons .cm-btn,
  #klaro .cookie-notice .cn-buttons .cm-link {
    width: auto !important;
    min-width: 0 !important;
    flex: 0 0 auto !important;
  }

  .klaro .cookie-modal .cm-list-input:checked + .cm-list-label .slider,
  .klaro .context-notice .cm-list-input:checked + .cm-list-label .slider,
  .klaro .cookie-notice .cm-list-input:checked + .cm-list-label .slider {
    background: var(--primary, #d97757) !important;
  }

  @media (max-width: 1024px) {
    #klaro .cookie-notice {
      max-width: calc(100vw - 2rem);
    }
  }
</style>

<script>
  var klaroConfig = {
    testing: false,
    elementID: "klaro",
    styling: {
      theme: ["light", "bottom", "left"]
    },
    storageMethod: "cookie",
    storageName: "klaro",
    htmlTexts: true,
    cookieExpiresAfterDays: 365,
    default: false,
    cookieDomain: ".heiwig.com",
    mustConsent: false,
    acceptAll: true,
    hideDeclineAll: true,
    hideLearnMore: false,
    noticeAsModal: false,
    translations: {
      de: {
        privacyPolicyUrl: "/privacy",
        consentNotice: {
          description: "🍪 Wir verwenden Cookies, um die Website zu verbessern. Details findest du in der Datenschutzerklarung.",
          learnMore: "Auswahlen"
        },
        ok: "Alles klar",
        consentModal: {
          title: "Cookie Einstellungen",
          description: "Hier kannst du einsehen und anpassen, welche Informationen wir sammeln."
        },
        purposes: {
          analytics: {
            title: "Besucher Statistiken"
          }
        }
      },
      en: {
        privacyPolicyUrl: "/privacy",
        consentNotice: {
          title: "Cookie Settings",
          description: "🍪 We use cookies to improve the website. Details are available in our privacy policy.",
          learnMore: "Select"
        },
        ok: "Accept all",
        consentModal: {
          title: "Consent Settings",
          description: "Here you can review and manage which information we collect."
        },
        purposes: {
          analytics: {
            title: "Analytics"
          }
        }
      }
    },
    services: [
      {
        name: "Google Analytics",
        default: false,
        translations: {
          de: {
            title: "Google Analytics",
            description: "Google Analytics ist ein Webanalyse Dienst der Google LLC."
          },
          en: {
            title: "Google Analytics",
            description: "Google Analytics is a web analytics service provided by Google LLC."
          }
        },
        purposes: ["analytics"]
      }
    ]
  };
</script>

<script>
  (function () {
    var script = document.createElement("script");
    script.src = "https://cdn.kiprotect.com/klaro/v0.7.18/klaro.js";
    script.onload = function () {
      if (window.klaro && typeof window.klaro.setup === "function") {
        window.klaro.setup(klaroConfig);
      }
    };
    document.head.appendChild(script);
  })();
</script>

Before moving on, make sure the domain and privacy URL are correct for your setup:

  • Replace cookieDomain with your production root domain if needed.
  • If your privacy page slug is not /privacy, update privacyPolicyUrl.
  • Keep this tag as the consent gate before any GA4 page view or event tags fire.
  • In GTM, open Advanced Settings -> Consent Settings and set this tag to No additional consent required (this banner tag itself must always load to collect consent).

Next, create explicit consent events so GTM can react immediately when users interact with the banner.

Create custom triggers

Add these triggers:

  1. Consent - Update Click Type: Click - All Elements Condition: Click Classes contains cm-btn

  2. Custom - Consent Updated Type: Custom Event Event name: consentUpdated

  3. Custom - Consent Choice Type: Custom Event Event name: consentChoice

These three triggers are the event backbone for consent updates.

Add the dataLayer push tag

Create another GTM Custom HTML tag:

  • Tag name: Consent cHTML - Datalayer push
  • HTML:
<script>
  (function () {
    window.setTimeout(function () {
      window.dataLayer.push({ event: "consentChoice" });
    }, 500);
    window.setTimeout(function () {
      window.dataLayer.push({ event: "consentUpdated" });
    }, 1000);
  })();
</script>
  • Consent setting: No additional consent required
  • Trigger: Consent - Update Click

Now map Klaro cookie values to GTM consent mode signals.

Create variables

Add these GTM variables:

  1. Cookie - Klaro Type: 1st Party Cookie Cookie Name: klaro

  2. GTM Consent State Type: Variable Template (from Gallery) Template Name: GTM Consent State Purpose: Store/read the current consent state.

  3. cjs - Analytics Consent Type: Custom JavaScript Code:

function () {
  var cookie = {{Cookie - Klaro}};
  if (!cookie) return "denied";

  var json = JSON.parse(cookie);
  if (json["Google Analytics"]) {
    return "granted";
  } else {
    return "denied";
  }
}

If you plan to use additional consent types (for example marketing/ad storage), create equivalent cjs variables for each consent mode signal you need.

Add a new tag from the GTM Gallery:

  • Tag template: Consent Mode (Google + Microsoft tags)
  • Purpose: Propagate the cjs variable values into GTM consent mode, so tags can be allow-listed or blocked automatically.

Map your cjs variables to the corresponding consent fields:

  • Analytics fields: map to cjs - Analytics Consent if you only use analytics.
  • Ads/personalization fields: map only if you actually collect those categories.
  • Functionality/security: usually granted, but you can also bind them to variables if required by your policy setup.

Trigger this tag on:

  1. Initialization - All Pages
  2. Custom - Consent Choice

Enable no-reload tracking updates

To avoid requiring a page reload after consent changes, add Custom - Consent Updated as an additional trigger to key pageview-related tags. This is exactly what the consentUpdated event is for.

Phase 4: Website Integration and GA4

Add GTM snippets in your website templates once, then keep ongoing tracking logic in GTM.

For website integration:

  • Place GTM <script> in <head> and GTM <noscript> right after opening <body>.
  • Keep container IDs environment-specific (staging vs production) if needed.
  • Avoid hardcoded analytics scripts in page templates when GTM is the source of truth.

For GA4 setup:

  • Create GA4 Configuration and event tags in GTM.
  • Ensure analytics tags respect consent signals.
  • Use GTM Preview plus GA4 DebugView to validate events and parameters.

Klaro Configuration

Klaro is the consent interface and source of consent state in this setup.

  • Keep service definitions aligned with the tools you actually run.
  • Keep translation text and privacy links up to date.
  • Keep defaults conservative and explicit.

Verification: What to Check in the GTM Debugger

  • Confirm Klaro - Consent Banner fires on Consent Initialization - All Pages.
  • Confirm Consent Mode (Google + Microsoft tags) fires on initialization and consentChoice.
  • Confirm Consent cHTML - Datalayer push fires on Consent - Update Click.
  • Confirm consentChoice and consentUpdated appear in dataLayer.
  • Confirm analytics tags fire only when expected after consent changes.
  • Verify this end-to-end in GTM Preview/Debugger by opening the website and checking each event flow.
  • Verify the consent banner is shown and that consent mode updates when you accept, decline, or change settings later.
  • Verify consent mode updates are reflected in Google Tag Manager and Google Analytics so tags are clearly fired or blocked based on consent.

In practice, this is very explicit in preview mode: you can see which tags fired, which did not fire, and which data layer events caused the change.

Wrap-Up

This setup works well because it separates stable platform code from operational tracking logic. Engineering keeps the foundation stable. Marketing can move quickly. Consent behavior stays visible and testable.

If the goal is to reduce friction without losing control, this GTM + Klaro model is a practical way to run tracking day to day and not paying a single cent to CMP platforms.

Back to home