CookieYes Consent Implementation in GTM
CCPA-Style CookieYes Consent Implementation in GTM — Opt-In by Default, With Sandboxed JS Doing the Heavy Lifting
CookieYes is a solid, low-effort consent-management platform, and its official GTM tag template (built and maintained by CookieYes themselves) will get most sites to "compliant enough" in ten minutes. But the free tier stops short of a few things a stricter CCPA implementation wants: a persistent "Do Not Sell or Share" link outside the banner itself, an enforced Global Privacy Control override, and fine-grained control over exactly when the banner UI is allowed to render. All of that is achievable from inside the CookieYes dashboard on a paid plan. This recipe shows how to do the same thing entirely from GTM — Custom HTML tags and one purpose-built Sandboxed JavaScript template — for the specific case where you're stuck with the free tier, or where a separate team owns the CookieYes configuration and isn't in a position (or willing) to make the changes for you.
⬇ Download the GTM Container JSON from Google DriveWhat's Inside the Container
The exported container (gtm-cookieyes-implementation.json, container nickname
scratchpad) holds 9 tags, 4 triggers, 42 variables, and 9
custom templates, spread across three folders. Most of the CookieYes-specific work lives
in one folder; a handful of reused utility templates and an unrelated Analytics/Microsoft Clarity
setup round out the rest of the container and aren't the focus of this post.
Folders
| Folder | Contains |
|---|---|
| CookieYes CMP | Every tag, trigger, and variable described in this post: the CMP tag itself, Consent Default/Update tags, the two hide/reveal Custom HTML tags, the footer link injector, the banner-interaction event tag, and the CookieYes cookie-parsing variables. |
| Element Hierarchy | Generic click-tracking helper variables (parent/grandparent class name) reused by the CookieYes banner-interaction trigger. Not CookieYes-specific on their own. |
| Analytics | GA4 configuration tag and shared event-settings variable, unrelated Session Cookie Parser and Timestamp utilities from an earlier recipe in this series — included in the export because the Google Tag Shared Event Settings variable is shared between recipes, but not covered here. |
Tags
| Tag | Type | Fires On | Purpose |
|---|---|---|---|
Google Analytics Configuration | Google Tag (googtag) | All Pages (built-in) | Standard GA4 config tag, consent status NOT_NEEDED since it relies on Consent Mode signals set elsewhere in the container rather than gating its own firing. |
CookieYes Hide Banner | Custom HTML (CSS) | Cookie Yes Page View | Force-hides .cky-consent-container and .cky-overlay with !important. Since this is a CCPA opt-out model, no interruptive opt-in banner is shown to any visitor by default — consent is granted unless a visitor finds the opt-out link. |
CookieYes Banner Load Listener | Custom HTML (JS) | Runs only as a Setup Tag for CookieYes CMP | Bridges CookieYes's native cookieyes_banner_load DOM event into the GTM dataLayer, so downstream triggers can react to it. |
CookieYes CMP | Custom Template (cvt_KDQSW, gallery) | Consent Initialization – All Pages | Injects the CookieYes client script, sets per-region default consent state, and re-applies the visitor's saved cookie state on repeat visits. |
CookieYes Consent Update | Custom Template (cvt_K8GSG, gallery) | Runs as the teardown/cleanup tag for CookieYes Consent Default — no trigger of its own | Pushes an updated gtag('consent', 'update', …) call, using the Consent State Reader variables, immediately after Consent Default finishes firing. |
CookieYes Add Consent Panel Footer Link | Custom HTML (JS) | CookieYes Insert Footer DOM Ready | Inserts a "Do Not Sell or Share My Personal Information" link into the Blogger footer, next to the native Blogger attribution link. Idempotent — only runs if the link isn't already present. |
CookieYes Disable Floating Widget | Custom HTML (CSS) | Cookie Yes Page View | Hides CookieYes's floating "revisit consent" widget button (.cky-btn-revisit-wrapper), since the footer link above is the intended re-entry point instead. |
CookieYes Consent Default | Custom Template (cvt_K8GSG, gallery) | CookieYes cookieyes_banner_load Event | Re-asserts regional default consent state (priority 99, fires early) whenever the native CookieYes banner-load event fires and the visitor has not yet acted. |
CookieYes Interaction Send Event | GA4 Event (gaawe) | CookieYes Click Interaction | Sends a GA4 cmp_click event with a cmp_interaction parameter whenever a visitor clicks anything inside the CookieYes banner, preference panel, or widget. |
Triggers
| Trigger | Type | Conditions |
|---|---|---|
CookieYes Insert Footer DOM Ready | DOM Ready | Is CookieYes Footer Link Present matches ^(undefined|null|0|false|NaN|)$ — i.e. only fires when the footer link does not already exist in the DOM, so a page re-render or SPA navigation can't duplicate it. |
Cookie Yes Page View | Pageview | Unconditional — fires on every page. |
CookieYes cookieyes_banner_load Event | Custom Event | Custom Event equals cookieyes_banner_load AND CookieYes isUserActionCompleted equals false — i.e. only before the visitor has made an explicit choice. |
CookieYes Click Interaction | Click – All Elements | CookieYes Element Interaction does not match ^(undefined|null|0|true|false|NaN|)$ — i.e. only fires when the lookup table below actually resolves a CookieYes-owned element. |
The CCPA Opt-Out Model: How Default Consent Is Set
The CookieYes CMP tag's regionSettings parameter takes a list of region groups,
each with its own default state for every Consent Mode signal. This container defines two groups,
built from seven Constant variables holding ISO country-code lists:
| Region Group | Countries (Constant Variables) | Default analytics_storage / ad_storage / ad_user_data / ad_personalization / personalization_storage |
|---|---|---|
| Opt-in required (GDPR-style) | CookieYes CMP Europe, West & Central Asia, Latin America, Africa | denied until the visitor actively opts in |
| Opt-out available (CCPA-style) | CookieYes CMP North America, Oceania, East & South Asia | granted until the visitor actively opts out |
security_storage is granted in every region — it isn't a marketing or
analytics signal, and Consent Mode always treats it as strictly necessary. Since this site's actual
audience is overwhelmingly US-based, the practical effect is: analytics and ads storage are on by
default, and a visitor has to find and use the opt-out link for anything to change.
The same default logic is duplicated in a second tag, CookieYes Consent Default,
built from the community Consent Mode (Google + Microsoft tags) template. It fires on the
native cookieyes_banner_load event specifically while the visitor hasn't yet acted,
re-asserting the same regional defaults. This is intentional belt-and-suspenders: the CookieYes CMP
tag sets defaults once at page load before its own script has necessarily executed, and the
Consent Default tag reinforces the same state the moment CookieYes's own script confirms it's
ready. Priority 99 keeps it firing ahead of anything else bound to that event.
Global Privacy Control as a Hard Override
Global Privacy Control (GPC) is a browser-level signal — a single boolean exposed at
navigator.globalPrivacyControl — that CCPA/CPRA and several other US state privacy
laws recognize as a legally valid opt-out request. A visitor who has GPC enabled is, legally
speaking, opting out the moment they land on the page, regardless of what any cookie says.
This container reads that signal with a plain GTM JavaScript Variable (not a custom script — GTM's built-in variable type reads a global object path directly) and uses its built-in value-formatting options to map the result cleanly:
Variable type: JavaScript Variable
Global Variable Name: navigator.globalPrivacyControl
Format value:
true → "denied"
false → "granted"
null / undefined → "granted"
That resolved granted/denied value is then given top precedence in two
If Else If – Advanced Lookup Table variables (a community template by
sublimetrix) that coalesce the
final ad_storage and analytics_storage values GTM actually uses:
Coalesce ad_storage — rule order (first match wins):
1. IF GPC signal = "denied" → return "denied"
2. IF CookieYes ad_storage cookie = "denied" → return "denied"
3. IF CookieYes ad_user_data cookie = "denied" → return "denied"
4. IF CookieYes ad_personalization cookie = "denied" → return "denied"
5. Default → "granted"
Coalesce analytics_storage — rule order (first match wins):
1. IF GPC signal = "denied" → return "denied"
2. IF CookieYes analytics_storage cookie = "denied" → return "denied"
3. Default → "granted"
The effect: a GPC signal always wins, a saved CookieYes "no" always wins over the CCPA default,
and in the absence of either, the visitor gets the CCPA opt-out default of granted.
This is the one place in the container where GTM logic actively overrides what CookieYes itself
would return — deliberately, since the free CookieYes tier doesn't expose GPC handling as a
toggle in its own dashboard.
Reading Consent Back Out of the CookieYes Cookie: A Sandboxed JS Template
CookieYes stores its own state in a first-party cookie named cookieyes-consent, as
a comma-separated string like necessary:yes,functional:yes,analytics:no,advertisement:no.
Rather than parsing that string out with a Regex Table variable per category — brittle, and it
duplicates the parsing logic seven times — I wrote a single reusable Sandboxed JavaScript
Custom Template called CookieYes Consent State Reader, and instantiated it seven
times (once per Google Consent Mode signal type) as a dropdown-configured variable.
// CookieYes Consent State Reader — Sandboxed JS (GTM Custom Template)
// Template parameters: consentType (SELECT), cookieName (TEXT, default
// "cookieyes-consent"), defaultConsent (RADIO: granted/denied)
const getCookieValues = require('getCookieValues');
const decodeUriComponent = require('decodeUriComponent');
const cookieName = data.cookieName || 'cookieyes-consent';
const consentType = data.consentType;
const defaultConsent = data.defaultConsent || 'granted';
if (!consentType) {
return 'denied';
}
// 1. Map a Google Consent Mode signal name to CookieYes's own category names
let targetCategory = '';
let fallbackCategory = '';
if (consentType === 'analytics_storage') {
targetCategory = 'analytics';
} else if (consentType === 'ad_storage' || consentType === 'ad_user_data' || consentType === 'ad_personalization') {
targetCategory = 'advertisement';
fallbackCategory = 'marketing';
} else if (consentType === 'personalization_storage') {
targetCategory = 'personalization';
} else if (consentType === 'functionality_storage') {
targetCategory = 'functional';
} else if (consentType === 'security_storage') {
targetCategory = 'necessary';
}
if (!targetCategory) {
return 'denied';
}
// 2. Read the cookie safely — absent cookie falls back to the CCPA default
const cookieValues = getCookieValues(cookieName);
if (!cookieValues || cookieValues.length === 0 || !cookieValues[0]) {
return defaultConsent;
}
const decodedCookie = decodeUriComponent(cookieValues[0]);
// 3. Parse "necessary:yes,functional:yes,analytics:no" into a lookup
const parts = decodedCookie.split(',');
let consentValue = defaultConsent;
for (let i = 0; i < parts.length; i++) {
const kv = parts[i].split(':');
if (kv.length === 2) {
const key = kv[0].trim();
const val = kv[1].trim();
if (key === targetCategory || (fallbackCategory && key === fallbackCategory)) {
consentValue = val === 'yes' ? 'granted' : (val === 'no' ? 'denied' : consentValue);
break;
}
}
}
return consentValue;
The template's Web Permissions are scoped tightly — it only requests
get_cookies access for the single named cookie (cookieyes-consent by
default), not open cookie access. Because it's a sandboxed template rather than a Custom HTML tag,
this parsing logic runs inside GTM's own sandbox and never touches the page's global scope or DOM.
Seven variables are built from this one template — one per Consent Mode signal
(ad_storage, ad_user_data, ad_personalization,
analytics_storage, personalization_storage, functionality_storage,
security_storage) — each just swapping the consentType dropdown. Two of
them (ad_storage and analytics_storage) then feed into the GPC-aware
coalesce logic above; the rest feed the CookieYes Consent Update tag directly.
Custom HTML: The Parts CookieYes's Free Tier Doesn't Cover
Four small Custom HTML tags do the work that a paid CookieYes plan would otherwise let you configure from its own dashboard. All four live in Blogger's page, injected by GTM rather than edited into the Blogger theme directly — useful if someone else owns theme access.
1. Suppressing the opt-in banner UI entirely. Since this container treats US traffic as opted-in by default, there's no reason to interrupt visitors with a banner asking them to opt in to something they're already opted into. This CSS-only tag hides both the banner and its overlay:
<style>
.cky-consent-container,
.cky-overlay {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
</style>
2. Hiding the floating "revisit consent" widget. CookieYes normally renders a small floating icon in the corner of the page that reopens the preference panel. This container instead surfaces that same functionality through a plain footer link (next), so the floating widget is suppressed:
<style>
.cky-btn-revisit-wrapper {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
</style>
3. Adding a persistent CCPA opt-out link to the Blogger footer. This is the piece a paid CookieYes plan would let you place through its own footer/legal-links configuration. Here it's inserted next to Blogger's own attribution link via plain DOM manipulation, and it's written to be idempotent — the DOM Ready trigger only fires it when the link isn't already present, so it survives re-renders without duplicating itself:
<script>
(function () {
var bloggerLink = document.querySelector('a[href*="blogger.com"]');
if (bloggerLink) {
var cookieLink = document.createElement('a');
cookieLink.href = '#';
cookieLink.className = 'cky-banner-element';
cookieLink.textContent = 'Do Not Sell or Share My Personal Information';
cookieLink.style.display = 'block';
cookieLink.style.marginTop = '8px';
var container = bloggerLink.closest('li, div, p') || bloggerLink.parentNode;
container.parentNode.insertBefore(cookieLink, container.nextSibling);
}
})();
</script>
Note the cky-banner-element class assigned to the new link — that's what makes
CookieYes treat a click on it as a banner interaction (it reopens the CookieYes preference panel),
and it's also what the click-tracking lookup table further down recognizes as a CookieYes-owned
element.
4. Bridging CookieYes's native event into the dataLayer. CookieYes dispatches
a native browser cookieyes_banner_load event, but that's invisible to GTM triggers
unless something republishes it into the dataLayer. This tiny listener does exactly that, and runs
as a Setup Tag ahead of the main CookieYes CMP tag rather than on its own trigger:
<script>
document.addEventListener('cookieyes_banner_load', function (eventData) {
window.dataLayer.push({ event: 'cookieyes_banner_load', cky: eventData.detail });
});
</script>
Tracking Banner and Widget Interactions in GA4
To know whether visitors are actually engaging with the consent UI at all — clicking the footer opt-out link, reopening preferences, or interacting with anything CookieYes renders — a Click trigger watches every click on the page and runs it through another If Else If lookup table that checks five different signals for a "cky" fingerprint:
CookieYes Element Interaction — rule order (first match wins):
1. Click Classes starts with "cky-"
2. Click Element's parent className starts with "cky-"
3. Click Element's grandparent className starts with "cky-"
4. Click ID starts with "cky"
5. Click URL contains "cdn-cookieyes.com/"
Checking parent and grandparent class names matters because CookieYes often renders its
interactive controls (toggles, buttons) as children of a container that carries the
cky- class, while the clicked element itself may not. Whatever the lookup table
resolves to is sent as the cmp_interaction parameter on a GA4 cmp_click
event, letting you build a funnel or exploration around how many visitors actually engage with the
opt-out mechanism versus how many never touch it.
Chaining Default → Update With a Teardown Tag, Not a Second Trigger
CookieYes Consent Update has no trigger of its own in this container, and that's intentional rather than a gap. It's configured as the teardown (cleanup) tag on CookieYes Consent Default — a GTM tag-level mechanic where a second tag is attached directly to a primary tag and fires automatically immediately after the primary tag finishes executing, with no separate trigger evaluation needed.
The sequence on every cookieyes_banner_load event (while the visitor hasn't yet
acted) is:
- CookieYes Consent Default fires first (priority 99), asserting the regional
defaultconsent state described above. - As soon as it completes successfully, its teardown tag — CookieYes Consent Update — fires
automatically, issuing a
gtag('consent', 'update', …)call built from the seven Consent State Reader variables reading the actualcookieyes-consentcookie. The teardown is configured withstopTeardownOnFailure: true, so if Consent Default fails to fire for any reason, Consent Update is skipped rather than firing with a possibly-inconsistent state.
Chaining the two this way — rather than giving Consent Update its own Custom Event trigger — guarantees strict ordering: the default is always established immediately before the real, cookie-derived state is layered on top of it, on the same event, with no race between two independently-evaluated triggers. It's a tidy pattern worth reaching for any time one tag's job is "finalize what the tag before it just started."
Community Custom Templates Used — Site Contributors Other Than Google
Every non-Google party who wrote code that this container executes, whether as a full Custom Template or as inline Custom HTML authored for this recipe:
| Template / Script | Author | Purpose |
|---|---|---|
CookieYes CMP (gallery template) | cookieyeshq | Injects the CookieYes client script; sets per-region default consent and applies saved cookie state on repeat visits. |
Consent Mode (Google + Microsoft tags) (gallery template) | Simo Ahava (gtm-templates-simo-ahava) | Used twice in this container — once configured for the default command, once for update — to issue Consent Mode v2 signals to both Google and Microsoft tags. |
CookieYes Consent State Reader | Kent Spencer (this recipe) | Sandboxed JS template that parses the cookieyes-consent cookie into a granted/denied value for any of the seven Consent Mode signal types. |
| Four Custom HTML tags (banner hide, widget hide, footer link, event bridge) | Kent Spencer (this recipe) | CSS and inline JavaScript replicating dashboard features not available on CookieYes's free tier — see code above. |
If Else If – Advanced Lookup Table | sublimetrix | Multi-condition lookup used for the GPC-aware consent coalescing and for the click-interaction fingerprinting. |
Two additional utility templates appear in the export (Timestamp by luratic and Get Root Domain by mbaersch), along with an unrelated Microsoft Clarity setup — all carried over from an earlier Analytics recipe in this series and not part of the CookieYes implementation covered here.
Why Sandboxed JS/Custom Templates Instead of Just Configuring CookieYes Directly
Everything in the "Custom HTML" and "Sandboxed JS" sections above — hiding the default banner UI, adding a footer opt-out link, honoring GPC as an override, tracking interaction events — is a checkbox or a text field inside CookieYes's own dashboard on a paid plan. I'm documenting the GTM version for one practical reason: on more than one project, a separate team owns the CMP-dashboard access has not been responsive, cooperative, or competent to make dashboard changes. These software behaviors can be fully implemented from inside the tag manager, with no dependency on anyone else's schedule. If you do control the CookieYes dashboard directly, the paid tier's built-in options will get you the same result with far less container complexity — use that instead.
How to Stage the JSON Export in GTM for Viewing
- Download
gtm-cookieyes-implementation.jsonfrom the Google Drive link above. - In GTM, open (or create) a container you're comfortable experimenting in — a scratch/sandbox container, not your live production container. This export's own container is literally named
scratchpadfor that reason. - Go to Admin → Import Container.
- Upload the JSON file.
- Choose a new Workspace for the import (rather than your current workspace) so you can review every tag, trigger, and variable in isolation before anything touches a live workspace.
- Choose Merge if you're importing into an existing container with content you want to keep, or Overwrite if it's a throwaway scratch container. Merge conflicts on variables sharing a name will prompt you to rename or overwrite each one individually.
- Do not publish. Use the Workspace's Tags/Triggers/Variables list and the "Summary" diff view to inspect everything the import brought in.
- Open Preview mode against a test URL to see the tags fire in sequence — this is the fastest way to confirm the Setup Tag ordering (Banner Load Listener → CookieYes CMP) and trigger conditions actually behave the way the tables above describe.
- Before using any of this for real: replace the placeholder
CookieYes Website Keyconstant with your own site's key and the placeholder GA4 Measurement IDs. Confirm the Consent Default → Consent Update teardown-tag chain in the tag's own configuration panel (the "Advanced Settings → Tag Sequencing" section of Consent Default) so you can see the cleanup relationship directly rather than taking this post's word for it. - Only publish once you've verified default consent state, the region groupings, and the GPC override behave as expected for your own audience and legal requirements — this container is a teaching example, not legal advice, and you are responsible for confirming it matches your own compliance obligations.
The full source — container JSON and this write-up — is also published on GitHub as a README. If you extend the region groupings, add another teardown-tag chain for a different event, or add handling for additional US state privacy signals, open a pull request or issue.
⬇ Download GTM Container JSON
Comments
Post a Comment