CookieYes Consent Implementation in GTM

CCPA-Style CookieYes Consent Implementation in GTM — Opt-In by Default, With Sandboxed JS Doing the Heavy Lifting

Published · Tags: GTM, CookieYes, Consent Mode, CCPA, GDPR, Global Privacy Control, Google Blogger, Sandboxed JavaScript, Custom HTML, GA4

Note: This is a hobby site based in the United States. Strictly speaking, a site like this is likely exempt from CCPA, GDPR, and similar regimes on size/commercial-purpose grounds alone. I built this recipe anyway, as a teaching example and as my own personal standard — partly because "we're probably exempt" is a bad long-term habit for anyone doing tag management professionally, and partly because it's a legitimately good structure to have on hand. The container is configured for a CCPA-style opt-out model: storage is granted by default for US/North America, Oceania, and East & South Asia visitors, and only moves to denied once a visitor actively withdraws consent (or a Global Privacy Control signal tells GTM to withdraw it for them). Visitors from GDPR-style regions (Europe, Latin America, Africa, West & Central Asia) get the opposite default: denied until they opt in. The JSON export can be downloaded from Google Drive below.

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 Drive

What'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

FolderContains
CookieYes CMPEvery 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 HierarchyGeneric click-tracking helper variables (parent/grandparent class name) reused by the CookieYes banner-interaction trigger. Not CookieYes-specific on their own.
AnalyticsGA4 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

TagTypeFires OnPurpose
Google Analytics ConfigurationGoogle 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 BannerCustom HTML (CSS)Cookie Yes Page ViewForce-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 ListenerCustom HTML (JS)Runs only as a Setup Tag for CookieYes CMPBridges CookieYes's native cookieyes_banner_load DOM event into the GTM dataLayer, so downstream triggers can react to it.
CookieYes CMPCustom Template (cvt_KDQSW, gallery)Consent Initialization – All PagesInjects the CookieYes client script, sets per-region default consent state, and re-applies the visitor's saved cookie state on repeat visits.
CookieYes Consent UpdateCustom Template (cvt_K8GSG, gallery)Runs as the teardown/cleanup tag for CookieYes Consent Default — no trigger of its ownPushes an updated gtag('consent', 'update', …) call, using the Consent State Reader variables, immediately after Consent Default finishes firing.
CookieYes Add Consent Panel Footer LinkCustom HTML (JS)CookieYes Insert Footer DOM ReadyInserts 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 WidgetCustom HTML (CSS)Cookie Yes Page ViewHides 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 DefaultCustom Template (cvt_K8GSG, gallery)CookieYes cookieyes_banner_load EventRe-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 EventGA4 Event (gaawe)CookieYes Click InteractionSends a GA4 cmp_click event with a cmp_interaction parameter whenever a visitor clicks anything inside the CookieYes banner, preference panel, or widget.

Triggers

TriggerTypeConditions
CookieYes Insert Footer DOM ReadyDOM ReadyIs 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 ViewPageviewUnconditional — fires on every page.
CookieYes cookieyes_banner_load EventCustom EventCustom Event equals cookieyes_banner_load AND CookieYes isUserActionCompleted equals false — i.e. only before the visitor has made an explicit choice.
CookieYes Click InteractionClick – All ElementsCookieYes 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 GroupCountries (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, Africadenied until the visitor actively opts in
Opt-out available (CCPA-style)CookieYes CMP North America, Oceania, East & South Asiagranted 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:

  1. CookieYes Consent Default fires first (priority 99), asserting the regional default consent state described above.
  2. 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 actual cookieyes-consent cookie. The teardown is configured with stopTeardownOnFailure: 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 / ScriptAuthorPurpose
CookieYes CMP (gallery template)cookieyeshqInjects 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 ReaderKent 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 TablesublimetrixMulti-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

  1. Download gtm-cookieyes-implementation.json from the Google Drive link above.
  2. 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 scratchpad for that reason.
  3. Go to Admin → Import Container.
  4. Upload the JSON file.
  5. 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.
  6. 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.
  7. Do not publish. Use the Workspace's Tags/Triggers/Variables list and the "Summary" diff view to inspect everything the import brought in.
  8. 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.
  9. Before using any of this for real: replace the placeholder CookieYes Website Key constant 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.
  10. 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

Popular posts from this blog

Site Landing & Site Referrer Preservation

GTM Browser Viewport Measurement

UTM & URL Query String 2 Cookies