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.
Pushing consent state to the dataLayer
Section titled “Pushing consent state to the dataLayer”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 callbackfunction 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→ readstrueorfalse - Variable name:
consent.advertising→ readstrueorfalse
Reading consent state in GTM triggers
Section titled “Reading consent state in GTM triggers”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:
- Create a Data Layer Variable: name it
DLV - Consent Analytics, Data Layer Variable Name:consent.analytics - On any trigger, add a condition:
DLV - Consent Analyticsequalstrue - 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.
Reading Google’s actual Consent Mode state
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 statefunction() { 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.
Conditional tag firing based on consent
Section titled “Conditional tag firing based on consent”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 Analyticsequalstrue
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 Analyticsequalstrue
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.
Custom consent categories
Section titled “Custom consent categories”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 recommendationsdata_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.
Consent state persistence across pages
Section titled “Consent state persistence across pages”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.
Pattern 1: CMP reads its own cookie on every page
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.
Pattern 3: localStorage bridge
Section titled “Pattern 3: localStorage bridge”// On CMP callback — save consent to localStoragefunction 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) {}})();Tracking consent change events
Section titled “Tracking consent change events”Log consent changes as GA4 events to audit your consent flow and monitor acceptance rates:
// In your CMP callback, push a consent_change eventfunction 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.
Common mistakes
Section titled “Common mistakes”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.