Skip to content

Consent State via DataLayer

Google’s Consent Mode handles consent for Google’s own tags. But what about your dataLayer-driven business logic — triggering non-Google tags, conditional event firing, consent state logging, or enterprise-specific consent categories that don’t map to Google’s seven types?

This article covers managing consent state as data in the dataLayer, which gives you full programmatic control over consent-aware tag firing regardless of what Google’s Consent Mode does underneath.

The simplest pattern: push your entire consent state to the dataLayer when the CMP callback fires. This makes consent state available as GTM variables.

// Called from your CMP's consent callback
function updateConsentDataLayer(consentChoices) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'consent_update',
'consent': {
'analytics': consentChoices.analytics, // true/false
'advertising': consentChoices.advertising, // true/false
'functional': consentChoices.functional, // true/false
'personalization': consentChoices.personalization // true/false
}
});
}

Once this push fires, you can read these values in GTM using Data Layer Variables:

  • Variable name: consent.analytics → reads true or false
  • Variable name: consent.advertising → reads true or false

With consent state in the dataLayer, you can use it as a trigger condition for any tag.

Using a Data Layer Variable in trigger conditions:

  1. Create a Data Layer Variable: name it DLV - Consent Analytics, Data Layer Variable Name: consent.analytics
  2. On any trigger, add a condition: DLV - Consent Analytics equals true
  3. The trigger only fires when analytics consent is granted

This approach works for any tag type — Custom HTML, GA4, third-party pixels, whatever.

Using a Custom JavaScript Variable as a consent gate:

// Custom JS Variable: "Consent Gate - Analytics"
function() {
var consentValue = {{DLV - Consent Analytics}};
// Handle both string and boolean representations
if (consentValue === true || consentValue === 'true') {
return true;
}
return false;
}

Add this variable as a trigger condition on any analytics-dependent tags.

Section titled “Reading Google’s actual Consent Mode state”

If you need to read the actual Consent Mode state that Google’s tags see, use the google_tag_data object:

// Custom JS Variable: reads actual Consent Mode state
function() {
try {
var consentEntries = window.google_tag_data &&
window.google_tag_data.ics &&
window.google_tag_data.ics.entries;
if (!consentEntries) return 'unknown';
// Returns 'granted' or 'denied' for analytics_storage
return consentEntries.analytics_storage &&
consentEntries.analytics_storage.update === 'granted'
? 'granted'
: 'denied';
} catch(e) {
return 'unknown';
}
}

This is useful when you want GTM logic to respond to the same consent state that Google’s tags see, rather than maintaining a parallel dataLayer-based consent state.

Here is the full pattern for gating a non-Google tag on analytics consent:

Step 1: Create a Data Layer Variable reading consent.analytics.

Step 2: Create a trigger:

  • Trigger Type: Custom Event
  • Event Name: consent_update
  • Trigger Conditions: DLV - Consent Analytics equals true

Step 3: Assign this trigger to your tag.

Step 4: Create an additional trigger for page views when consent is already granted (returning visitors who accepted previously):

  • Trigger Type: Page View
  • Trigger Conditions: DLV - Consent Analytics equals true

The second trigger handles returning visitors where consent was restored from a cookie before the first page view. Without it, returning consenting visitors miss the first pageview event.

Enterprise implementations often have consent categories that go beyond Google’s seven types. A media company might have:

  • editorial_analytics — tracking content engagement (separate from advertising)
  • personalization — content recommendations
  • data_enrichment — appending CRM data to events

Push these custom categories alongside Google’s standard types:

window.dataLayer.push({
'event': 'consent_update',
'consent': {
// Standard Google Consent Mode types (handled via gtag)
'analytics_storage': 'granted',
'ad_storage': 'denied',
// Custom enterprise categories
'editorial_analytics': true,
'content_personalization': false,
'data_enrichment': false
}
});

GTM variables can read consent.editorial_analytics and use it in trigger conditions, while the standard Google Consent Mode update handles the Google-specific types separately.

The dataLayer resets on every hard page navigation. Consent state pushed in the CMP callback only persists for that page load. On the next page, the CMP callback fires again and re-establishes consent state — but there is a gap between page load and when the CMP callback fires.

For SPAs (single-page applications), this is not an issue — the dataLayer persists for the lifetime of the page. But for traditional multi-page sites, handle the persistence explicitly.

Section titled “Pattern 1: CMP reads its own cookie on every page”

Most CMPs handle this automatically. On every page load, the CMP reads its stored consent cookie and fires the consent update callback immediately. The gap between page load and CMP callback is typically 50-200ms.

Verify this by opening a page in a browser that already has a consent cookie and checking how quickly the consent update event appears in GTM Preview mode.

Pattern 2: dataLayer pre-population from server

Section titled “Pattern 2: dataLayer pre-population from server”

For sites that render server-side, you can inject consent state into the initial dataLayer push from the server:

<script>
window.dataLayer = window.dataLayer || [];
// Server injects the user's stored consent state
window.dataLayer.push({
'consent': {
'analytics': <%= user.consent.analytics %>,
'advertising': <%= user.consent.advertising %>
}
});
</script>

This gives you consent state available immediately, before any JavaScript runs. The CMP callback can then update it when it loads.

// On CMP callback — save consent to localStorage
function onConsentUpdate(choices) {
try {
localStorage.setItem('consent_state', JSON.stringify({
analytics: choices.analytics,
advertising: choices.advertising,
timestamp: Date.now()
}));
} catch(e) {}
// Push to dataLayer as usual
window.dataLayer.push({
'event': 'consent_update',
'consent': choices
});
}
// On page load — restore consent from localStorage before GTM processes
(function restoreConsent() {
try {
var stored = JSON.parse(localStorage.getItem('consent_state') || 'null');
if (!stored) return;
// Only use stored consent if less than 13 months old (GDPR reconsent period)
var thirteenMonths = 13 * 30 * 24 * 60 * 60 * 1000;
if (Date.now() - stored.timestamp > thirteenMonths) {
localStorage.removeItem('consent_state');
return;
}
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'consent': {
'analytics': stored.analytics,
'advertising': stored.advertising
}
});
} catch(e) {}
})();

Log consent changes as GA4 events to audit your consent flow and monitor acceptance rates:

// In your CMP callback, push a consent_change event
function onConsentUpdate(newChoices, previousChoices) {
window.dataLayer.push({
'event': 'consent_change',
'consent_analytics': newChoices.analytics ? 'granted' : 'denied',
'consent_advertising': newChoices.advertising ? 'granted' : 'denied',
'consent_previous_analytics': previousChoices ? previousChoices.analytics : 'unknown',
'consent_action': determineAction(newChoices, previousChoices)
});
}
function determineAction(newChoices, prev) {
if (!prev) return 'initial_choice';
var prevGranted = prev.analytics || prev.advertising;
var newGranted = newChoices.analytics || newChoices.advertising;
if (!prevGranted && newGranted) return 'consent_granted';
if (prevGranted && !newGranted) return 'consent_revoked';
return 'consent_updated';
}

Create a GA4 event tag triggered on consent_change to send these events. This gives you consent flow data in GA4 — useful for identifying where users abandon the consent banner and whether your banner design affects acceptance rates.

Reading consent state too early. Data Layer Variables read the current merged state, not the state at any specific moment. If a tag evaluates consent.analytics before the CMP callback fires, it may read undefined rather than the correct value.

Expecting dataLayer variables to update automatically. When consent changes, you must push a new event with updated consent values. Variables read the merged state, which is updated on each push. Without a new push, variables reflect the old state.

Not accounting for the undefined case. If consent state hasn’t been pushed yet, Data Layer Variables return undefined. Trigger conditions using equals true correctly exclude undefined. But conditions using does not equal false incorrectly include undefined. Use explicit equals true or equals false comparisons.