Skip to main content

Tag managers

Google Tag Manager

Configure Google Tag Manager

Copy the container ID, which starts with GTM-, from your GTM workspace. Remove the existing GTM snippet, its <noscript> iframe and any framework plugin that loads the same container.

npm install @c15t/integrations@alpha
src/consent-scripts.ts
import { googleTagManager } from '@c15t/integrations/google-tag-manager';

export const scripts = [googleTagManager({ id: 'GTM-XXXXXXX' })];

Register the scripts

Complete your framework quickstart first. Keep its Inth endpoint, policy, styles and consent UI. Remove the vendor's original script, SDK initializer or tag-manager entry, so the vendor loads only through c15t.

The vendor pages put the helper in src/consent-scripts.ts. If your framework quickstart already has a scripts array, such as the one in c15t.config.ts in the Next.js guide, add the helper to that array instead of creating a second file. The scripts export is a configuration, not an initializer. Add it to the c15t provider you already have, at the registration point for your framework below. These are edits to that provider, not a second provider.

Add the configuration to scripts in c15t.config.ts, next to next.config.ts:

import { defineConsentConfig } from 'c15t/next';
import { scripts } from './src/consent-scripts';

export default defineConsentConfig({ scripts });

Keep the rest of your config, such as mode and routePrefix, in the same call. ConsentRoot reads the config in the browser, so the layout keeps passing only state. App Router, Pages Router and static export all read the same file. See Next.js scripts and embeds.

Options

OptionDefaultBehavior
idRequiredContainer ID, for example GTM-1234XXX. The helper trims it. Empty or whitespace-only values log an error and the script does not load.
dataLayer'dataLayer'Queue name. A custom name adds &l=<name> to the container URL and renames the helper's queue function to <name>Gtag.
updateEventName'consent-update'dataLayer event pushed after every consent update. Use it as a GTM trigger.
consentMappingThe table belowReplaces the category-to-Google mapping.
loadMode'always'When gtm.js loads. See choose when the container loads.
category'necessary', or { or: ['measurement', 'marketing'] } with loadMode: 'after-consent'Consent condition for the container. With 'after-consent', gtm.js waits for it. With 'always', it sets the script's permission, which callbacks receive, but does not delay loading.

Choose when the container loads

loadModeUntil the category is allowedAfter the category is allowed
'always'Loads gtm.js and sends consent default with the current permissions before the gtm.js start event.Sends consent update, then the consent-update event.
'after-consent'Sends nothing to Google. The helper creates no dataLayer or gtag function.Loads gtm.js once. consent default carries the current permissions and comes before the gtm.js start event. Later changes send update and consent-update.

Use 'after-consent' when your policy forbids any request to Google before the visitor opts in:

src/consent-scripts.ts (partial)
googleTagManager({ id: 'GTM-XXXXXXX', loadMode: 'after-consent' }),

'after-consent' waits for the category to be allowed, not for a recorded choice. Under an opt-in policy, that happens when the visitor allows it. Under an opt-out or none policy, optional categories are allowed before a choice, so gtm.js loads on the first page unless a saved refusal or a privacy signal restricts the category; see policies.

necessary is always allowed, so a gated container needs another category. This mode defaults to { or: ['measurement', 'marketing'] }. A container usually holds Analytics tags and advertising tags, and this default loads it once the visitor allows either purpose. Google tags inside still follow the Consent Mode types, so an Ads tag stays restricted for a visitor who allowed only measurement. If the container holds only Analytics tags, pass category: 'measurement'.

This mode gives up part of Consent Mode. With 'always', Google tags send cookieless pings while a type is denied, and Google uses them to model conversions and behavior for visitors who refused or have not chosen. With 'after-consent', visitors whose category is denied send nothing, so Google has no data to model them from. Reports cover only visitors who allowed the category.

The container starts with the visitor's current choice in its default command. On the page where the visitor accepts, it loads after the choice, so no consent-update event fires for that choice. Triggers that wait only for consent-update first fire on a later change. Use the tag's consent settings, described below, for tags that should fire on the first page.

When the visitor withdraws the category, c15t reloads the page and the new page does not load gtm.js. With reloadOnConsentRevoked: false, the running container stays on the page and gets an update that denies the withdrawn types. A later grant reuses it instead of starting a second container.

Google loads before a choice by default

With the default loadMode: 'always', the googleTagManager and gtag helpers set alwaysLoad: true. Before the visitor chooses, the helper creates the dataLayer queue, sends gtag('consent', 'default', ...) with the current permissions and loads Google's script. After each permission change it sends gtag('consent', 'update', ...). Google's tags then adjust what they store and send; see Google's Consent Mode overview.

So the browser contacts Google before consent. If your policy requires no Google request until the visitor allows it, set loadMode: 'after-consent'. The helper then creates nothing and requests nothing until its category is allowed. When it loads, it sends the default command first, with the permissions at that moment, and update commands after later changes. Under an opt-out or none policy, optional categories are allowed before a choice, so the helper loads on the first page.

c15t categoryGoogle consent types
necessarysecurity_storage
functionalityfunctionality_storage
measurementanalytics_storage
marketingad_storage, ad_user_data, ad_personalization
experiencepersonalization_storage

Each Google type is granted when its category is allowed and denied otherwise. If a visitor turns off the helper's vendor, every optional type is sent as denied. The consentMapping option replaces the whole table, so include every category you still want signalled.

Under an opt-out policy, the default command can grant types before the visitor has chosen anything. That is a permission, not a recorded choice.

With the default loadMode: 'always', googleTagManager uses the necessary category and alwaysLoad, so the container loads on every page. With 'after-consent', it loads once its category is allowed. In both modes, consent signals reach Google tags that support Consent Mode. Other tags in the container, such as a third-party pixel added as Custom HTML, ignore those signals and fire on their triggers.

For each non-Google tag, add a consent requirement in the tag's consent settings, or fire it from a Custom Event trigger on consent-update. If a vendor must make no request before consent, remove it from the container and register its own c15t helper instead.

Measure opt-in rate

If you run a banner experiment, the backend already counts visitors and choices per arm. To see the arm in GTM as well, forward the onSurfaceShown and onChoiceRecorded callbacks, which carry experiment: { id, arm }. See banner experiments.

Verify Google Tag Manager

With the default loadMode: 'always':

  1. In a private window with an opt-in policy, load the page. gtm.js loads. In Google Tag Assistant, the first consent command is default with analytics_storage and ad_storage set to denied.
  2. Click Reject, then reload. The default command still denies every optional type, and in DevTools Network no non-Google tag in the container fires.
  3. Open Privacy settings and allow measurement. Tag Assistant shows an update command with analytics_storage set to granted, followed by a consent-update event, and measurement tags fire.
  4. Turn measurement off again and save. c15t reloads the page, and the new page starts with analytics_storage denied.

With loadMode: 'after-consent':

  1. In a private window with an opt-in policy, filter DevTools Network by google and load the page. No request appears, and window.dataLayer is undefined in the Console.
  2. Allow measurement. gtm.js loads once. In Tag Assistant, the first command is consent default with analytics_storage set to granted, before the container starts.
  3. Allow marketing as well. Tag Assistant shows an update command that grants the ad types, followed by a consent-update event. No second gtm.js request appears.
  4. Turn both off again and save. c15t reloads the page, and the new page makes no request to Google.

See the consent verification guide for navigation and hosting checks.