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:
- Klaro is loaded via GTM as early as possible.
- Consent choices are stored in the Klaro cookie.
- GTM variables read that cookie and convert values to consent mode signals.
- Consent Mode updates what can fire (analytics, ads, personalization, etc.).
- 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
cookieDomainwith your production root domain if needed. - If your privacy page slug is not
/privacy, updateprivacyPolicyUrl. - Keep this tag as the consent gate before any GA4 page view or event tags fire.
- In GTM, open
Advanced Settings->Consent Settingsand set this tag toNo additional consent required(this banner tag itself must always load to collect consent).
Phase 2: Make Consent Changes Observable in GTM
Next, create explicit consent events so GTM can react immediately when users interact with the banner.
Create custom triggers
Add these triggers:
Consent - Update ClickType:Click - All ElementsCondition:Click Classescontainscm-btnCustom - Consent UpdatedType:Custom EventEvent name:consentUpdatedCustom - Consent ChoiceType:Custom EventEvent 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
Phase 3: Translate Cookie State into Consent Mode
Now map Klaro cookie values to GTM consent mode signals.
Create variables
Add these GTM variables:
Cookie - KlaroType:1st Party CookieCookie Name:klaroGTM Consent StateType:Variable Template(from Gallery) Template Name:GTM Consent StatePurpose: Store/read the current consent state.cjs - Analytics ConsentType:Custom JavaScriptCode:
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 the Consent Mode template tag
Add a new tag from the GTM Gallery:
- Tag template:
Consent Mode (Google + Microsoft tags) - Purpose: Propagate the
cjsvariable 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 Consentif 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:
Initialization - All PagesCustom - 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 Bannerfires onConsent Initialization - All Pages. - Confirm
Consent Mode (Google + Microsoft tags)fires on initialization andconsentChoice. - Confirm
Consent cHTML - Datalayer pushfires onConsent - Update Click. - Confirm
consentChoiceandconsentUpdatedappear indataLayer. - 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.