@c15t/react 3.0.0-alpha.8 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/AGENTS.md +10 -12
  2. package/README.md +41 -70
  3. package/SKILL.md +1 -1
  4. package/dist/aggregate-components.js +1 -1
  5. package/dist/components/consent-gate/atoms.js +1 -1
  6. package/dist/components/consent-gate/frame.js +2 -0
  7. package/dist/components/consent-gate/index.js +1 -1
  8. package/dist/components/iab-panel/atoms/root.js +1 -1
  9. package/dist/components/iab-panel/iab-panel.js +1 -1
  10. package/dist/components/iab-prompt/atoms/root.js +1 -1
  11. package/dist/components/iab-prompt/iab-prompt.js +1 -1
  12. package/dist/components/panel/atoms/root.js +1 -1
  13. package/dist/components/panel-trigger/atoms/root.js +1 -1
  14. package/dist/components/panel-trigger/panel-trigger.js +1 -1
  15. package/dist/components/panel-trigger/toolbar-loader.js +2 -0
  16. package/dist/components/preferences/atoms/root.js +1 -1
  17. package/dist/components/prompt/atoms/early-tap-script.js +2 -0
  18. package/dist/components/prompt/atoms/root.js +1 -1
  19. package/dist/components/prompt/prompt.js +1 -1
  20. package/dist/components/shared/dialog-sheets.js +1 -0
  21. package/dist/components/shared/first-paint-sheets.js +1 -0
  22. package/dist/components/shared/iab-dialog-sheets.js +1 -0
  23. package/dist/components/shared/iab-first-paint-sheets.js +1 -0
  24. package/dist/components/shared/primitives/button.js +1 -1
  25. package/dist/components/shared/surface-styles.js +2 -0
  26. package/dist/consent-gate.js +1 -1
  27. package/dist/context/index.js +1 -1
  28. package/dist/devtools.js +1 -1
  29. package/dist/hooks/use-iab-policy-guard.js +1 -0
  30. package/dist/iab/styles.tw3.css +2 -2
  31. package/dist/iab-context.js +1 -1
  32. package/dist/iab-dialog-deferred.js +2 -0
  33. package/dist/iab.js +1 -1
  34. package/dist/index-parts/consent-gate.js +1 -1
  35. package/dist/index-parts/core.js +1 -1
  36. package/dist/index-parts/frame.js +1 -0
  37. package/dist/index-parts/hosted.js +1 -0
  38. package/dist/index-parts/offline.js +1 -0
  39. package/dist/index-parts/presets.js +1 -0
  40. package/dist/index.js +1 -1
  41. package/dist/modes.js +1 -0
  42. package/dist/panel-link.js +1 -1
  43. package/dist/panel-trigger.js +1 -1
  44. package/dist/provider.js +1 -1
  45. package/dist/styles.tw3.css +2 -2
  46. package/dist/transports/hosted.js +1 -0
  47. package/dist/transports/manifest.js +1 -0
  48. package/dist/utils/deferred-module.js +1 -1
  49. package/dist/version.js +1 -1
  50. package/dist-types/chunk-warming.d.ts +7 -6
  51. package/dist-types/components/consent-gate/frame.d.ts +19 -0
  52. package/dist-types/components/consent-gate/index.d.ts +3 -19
  53. package/dist-types/components/iab-prompt/iab-prompt.d.ts +1 -1
  54. package/dist-types/components/panel/index.d.ts +1 -1
  55. package/dist-types/components/panel-trigger/toolbar-loader.d.ts +34 -0
  56. package/dist-types/components/preferences/index.d.ts +1 -1
  57. package/dist-types/components/prompt/atoms/early-tap-script.d.ts +17 -0
  58. package/dist-types/components/prompt/index.d.ts +1 -1
  59. package/dist-types/components/shared/dialog-sheets.d.ts +9 -0
  60. package/dist-types/components/shared/first-paint-sheets.d.ts +8 -0
  61. package/dist-types/components/shared/iab-dialog-sheets.d.ts +3 -0
  62. package/dist-types/components/shared/iab-first-paint-sheets.d.ts +3 -0
  63. package/dist-types/components/shared/surface-styles.d.ts +60 -0
  64. package/dist-types/consent-gate.d.ts +1 -1
  65. package/dist-types/context/index.d.ts +3 -3
  66. package/dist-types/context.d.ts +2 -0
  67. package/dist-types/gpp.d.ts +1 -1
  68. package/dist-types/hooks/use-iab-policy-guard.d.ts +35 -0
  69. package/dist-types/iab-dialog-deferred.d.ts +36 -0
  70. package/dist-types/iab.d.ts +3 -2
  71. package/dist-types/index-parts/consent-gate.d.ts +1 -1
  72. package/dist-types/index-parts/core.d.ts +1 -2
  73. package/dist-types/index-parts/frame.d.ts +7 -0
  74. package/dist-types/index-parts/hosted.d.ts +7 -0
  75. package/dist-types/index-parts/offline.d.ts +7 -0
  76. package/dist-types/index-parts/presets.d.ts +7 -0
  77. package/dist-types/index.d.ts +5 -1
  78. package/dist-types/modes.d.ts +26 -0
  79. package/dist-types/panel-link.d.ts +1 -1
  80. package/dist-types/panel-trigger.d.ts +1 -1
  81. package/dist-types/provider.d.ts +22 -4
  82. package/dist-types/transports/hosted.d.ts +36 -0
  83. package/dist-types/transports/manifest.d.ts +48 -0
  84. package/dist-types/ui-config-context.d.ts +4 -0
  85. package/dist-types/version.d.ts +1 -1
  86. package/docs/README.md +10 -12
  87. package/docs/concepts/choose-your-setup.md +45 -8
  88. package/docs/concepts/consent-categories.md +1 -1
  89. package/docs/concepts/consent-state.md +5 -2
  90. package/docs/concepts/data-fetching.md +100 -40
  91. package/docs/concepts/modes.md +281 -0
  92. package/docs/customization/dark-mode.md +1 -1
  93. package/docs/customization/motion.md +2 -2
  94. package/docs/customization/overview.md +3 -2
  95. package/docs/customization/recipes.md +4 -4
  96. package/docs/customization/slots.md +25 -25
  97. package/docs/customization/stylesheets.md +161 -50
  98. package/docs/customization/tailwind.md +71 -50
  99. package/docs/customization/tokens.md +42 -13
  100. package/docs/frameworks/react/components/consent-banner.md +14 -12
  101. package/docs/frameworks/react/components/consent-dialog.md +15 -13
  102. package/docs/frameworks/react/components/consent-provider.md +51 -46
  103. package/docs/frameworks/react/components.md +16 -13
  104. package/docs/frameworks/react/compose.md +3 -3
  105. package/docs/frameworks/react/content-security-policy.md +35 -12
  106. package/docs/frameworks/react/customize.md +21 -13
  107. package/docs/frameworks/react/hooks.md +2 -2
  108. package/docs/frameworks/react/iab.md +56 -6
  109. package/docs/frameworks/react/quickstart.md +65 -99
  110. package/docs/frameworks/react/rendering.md +170 -94
  111. package/docs/frameworks/react/scripts.md +19 -30
  112. package/docs/frameworks/react/troubleshooting.md +100 -32
  113. package/docs/frameworks/react/upgrade-v3.md +133 -79
  114. package/docs/frameworks/react/vendor-consent.md +19 -7
  115. package/docs/guides/banner-experiments.md +21 -8
  116. package/docs/guides/troubleshooting.md +90 -26
  117. package/docs/guides/verify-consent.md +23 -8
  118. package/docs/integrations/adobe-analytics.md +60 -64
  119. package/docs/integrations/ahrefs-analytics.md +58 -63
  120. package/docs/integrations/amplitude.md +60 -64
  121. package/docs/integrations/clearbit.md +59 -63
  122. package/docs/integrations/cloudflare-web-analytics.md +56 -60
  123. package/docs/integrations/cloudflare-zaraz.md +55 -57
  124. package/docs/integrations/crisp.md +64 -69
  125. package/docs/integrations/databuddy.md +62 -64
  126. package/docs/integrations/fathom-analytics.md +56 -60
  127. package/docs/integrations/front-chat.md +56 -60
  128. package/docs/integrations/google-maps.md +25 -13
  129. package/docs/integrations/google-tag-manager.md +153 -77
  130. package/docs/integrations/google-tag.md +134 -78
  131. package/docs/integrations/heap.md +56 -60
  132. package/docs/integrations/hightouch.md +55 -60
  133. package/docs/integrations/hotjar.md +60 -64
  134. package/docs/integrations/intercom.md +55 -60
  135. package/docs/integrations/klaviyo.md +65 -66
  136. package/docs/integrations/linkedin-insights.md +58 -63
  137. package/docs/integrations/logrocket.md +55 -60
  138. package/docs/integrations/matomo-analytics.md +58 -60
  139. package/docs/integrations/meta-pixel.md +56 -60
  140. package/docs/integrations/microsoft-clarity.md +57 -60
  141. package/docs/integrations/microsoft-uet.md +58 -61
  142. package/docs/integrations/mixpanel-analytics.md +73 -75
  143. package/docs/integrations/one-dollar-stats.md +53 -59
  144. package/docs/integrations/openai-pixel.md +58 -60
  145. package/docs/integrations/overview.md +59 -42
  146. package/docs/integrations/pinterest-tag.md +56 -60
  147. package/docs/integrations/pirsch.md +54 -60
  148. package/docs/integrations/plausible-analytics.md +55 -60
  149. package/docs/integrations/posthog.md +56 -60
  150. package/docs/integrations/promptwatch.md +57 -63
  151. package/docs/integrations/reddit-pixel.md +57 -60
  152. package/docs/integrations/rudderstack.md +65 -67
  153. package/docs/integrations/rybbit-analytics.md +70 -75
  154. package/docs/integrations/segment.md +56 -60
  155. package/docs/integrations/sentry.md +65 -60
  156. package/docs/integrations/snapchat-pixel.md +54 -60
  157. package/docs/integrations/tiktok-pixel.md +59 -63
  158. package/docs/integrations/umami-analytics.md +54 -60
  159. package/docs/integrations/vercel-analytics.md +53 -59
  160. package/docs/integrations/vercel-speed-insights.md +317 -0
  161. package/docs/integrations/x-pixel.md +57 -63
  162. package/docs/integrations/youtube.md +25 -13
  163. package/package.json +18 -8
  164. package/docs/upgrade-v3.md +0 -429
@@ -1,24 +1,42 @@
1
1
  ---
2
2
  title: Google Tag Manager
3
- description: Load a Google Tag Manager container with c15t Consent Mode v2
4
- signals, configure consent checks inside the container, and verify both in
5
- DevTools.
3
+ description: Load a Google Tag Manager container before or after consent with
4
+ c15t Consent Mode v2 signals, configure consent checks inside the container,
5
+ and verify both in DevTools.
6
+ seoTitle: Google Tag Manager Consent Mode v2 with c15t
7
+ seoDescription: Set Google Consent Mode v2 defaults before your GTM container
8
+ starts. Load it on every page or only after consent with the c15t
9
+ googleTagManager helper.
6
10
  icon: google-tag-manager
7
11
  group: integrations
8
12
  ---
9
13
 
14
+ The c15t `googleTagManager` helper loads your Google Tag Manager container,
15
+ `gtm.js`. With the default `loadMode: 'always'`, the container loads on every
16
+ page, before the visitor chooses, under the `necessary` category. With
17
+ `loadMode: 'after-consent'`, c15t sends nothing to Google until the helper's
18
+ `category` has consent, which defaults to `measurement` or `marketing`. In both
19
+ modes, the helper sends the Google Consent Mode v2 default with
20
+ `gtag('consent', 'default', ...)` before the container starts. Once the
21
+ container runs, each later consent change sends an `update`, and then a
22
+ `consent-update` event.
23
+ `measurement` controls `analytics_storage`, and `marketing` controls
24
+ `ad_storage`, `ad_user_data`, and `ad_personalization`. Tags in the container
25
+ that are not from Google ignore these signals, unless you add consent checks in
26
+ GTM.
27
+
10
28
  ## Configure Google Tag Manager
11
29
 
12
30
  Copy the container ID, which starts with `GTM-`, from your GTM workspace.
13
31
  Remove the existing GTM snippet, its `<noscript>` iframe and any framework
14
32
  plugin that loads the same container.
15
33
 
16
- | Package manager | Command |
17
- | :-------------- | :------------------------------------- |
18
- | npm | `npm install @c15t/integrations@alpha` |
19
- | pnpm | `pnpm add @c15t/integrations@alpha` |
20
- | yarn | `yarn add @c15t/integrations@alpha` |
21
- | bun | `bun add @c15t/integrations@alpha` |
34
+ | Package manager | Command |
35
+ | :-------------- | :------------------------------- |
36
+ | npm | `npm install @c15t/integrations` |
37
+ | pnpm | `pnpm add @c15t/integrations` |
38
+ | yarn | `yarn add @c15t/integrations` |
39
+ | bun | `bun add @c15t/integrations` |
22
40
 
23
41
  ```ts title="src/consent-scripts.ts"
24
42
  import { googleTagManager } from '@c15t/integrations/google-tag-manager';
@@ -33,51 +51,41 @@ endpoint, policy, styles and consent UI. Remove the vendor's original script,
33
51
  SDK initializer or tag-manager entry, so the vendor loads only through c15t.
34
52
 
35
53
  The vendor pages put the helper in `src/consent-scripts.ts`. If your framework
36
- quickstart already created a scripts file, such as `lib/scripts.ts` in the
37
- Next.js guide, add the helper to that array instead of creating a second file.
54
+ quickstart already has a `scripts` array, such as the one in `c15t.config.ts`
55
+ in the Next.js guide, add the helper to that array instead of creating a
56
+ second file.
38
57
  The `scripts` export is a configuration, not an initializer. Add it to the c15t provider you already have, at the registration
39
58
  point for your framework below. These are edits to that provider, not a second
40
59
  provider.
41
60
 
42
61
  **Next.js**
43
62
 
44
- Import the configuration into the client boundary from your router guide:
63
+ Add the configuration to `scripts` in `c15t.config.ts`, next to
64
+ `next.config.ts`:
45
65
 
46
- ```ts
47
- import { ConsentRoot } from 'c15t/next';
48
- import { scripts } from './consent-scripts';
49
- ```
66
+ ```ts title="c15t.config.ts"
67
+ import { defineConsentConfig } from 'c15t/next';
68
+ import { scripts } from './src/consent-scripts';
50
69
 
51
- Keep the server-resolved `state` and shared `consentConfig` from your
52
- router guide. Its manifest, init and save URLs stay in effect. Add
53
- `scripts` as a top-level prop on the existing root:
54
-
55
- ```tsx
56
- <ConsentRoot state={state} config={consentConfig} scripts={scripts}>
57
- {children}
58
- </ConsentRoot>
70
+ export default defineConsentConfig({ scripts });
59
71
  ```
60
72
 
61
- App Router, Pages Router and static export all use this `ConsentRoot` in
62
- the `'use client'` wrapper `components/consent.tsx`. Keep `scripts` there,
63
- because a Server Component cannot pass script callbacks to it. See
73
+ Keep the rest of your config, such as `mode` and `routePrefix`, in the
74
+ same call. `ConsentRoot` reads the config in the browser, so the layout
75
+ keeps passing only `state`. App Router, Pages Router and static export all
76
+ read the same file. See
64
77
  [Next.js scripts and embeds](https://c15t.com/docs/frameworks/next/scripts).
65
78
 
66
79
  **TanStack Start**
67
80
 
68
81
  Import the configuration into your root route and pass it to the existing
69
- `ConsentRoot` as a top-level prop. Keep the loader, `backendURL` and
70
- `initRoute` from the [TanStack Start quickstart](https://c15t.com/docs/frameworks/tanstack-start/quickstart):
82
+ `ConsentRoot` as a top-level prop. Keep the loader from the
83
+ [TanStack Start quickstart](https://c15t.com/docs/frameworks/tanstack-start/quickstart):
71
84
 
72
85
  ```tsx title="src/routes/__root.tsx"
73
86
  import { scripts } from '../consent-scripts';
74
87
 
75
- <ConsentRoot
76
- state={consent}
77
- backendURL={backendURL}
78
- initRoute={false}
79
- scripts={scripts}
80
- >
88
+ <ConsentRoot state={consent} scripts={scripts}>
81
89
  ```
82
90
 
83
91
  Import vendor helpers in the root route module, not in a server function.
@@ -96,8 +104,8 @@ import { scripts } from './consent-scripts';
96
104
  <ConsentProvider options={{ mode, scripts }}>
97
105
  ```
98
106
 
99
- `mode` is the `hosted({ url: 'https://your-project.inth.app' })` value
100
- from the [React quickstart](../frameworks/react/quickstart.md). Keep the banner,
107
+ `mode` is the `manifest()` value from the
108
+ [React quickstart](../frameworks/react/quickstart.md). Keep the banner,
101
109
  dialog and preferences link inside the provider. See
102
110
  [React scripts and embeds](../frameworks/react/scripts.md).
103
111
 
@@ -126,15 +134,16 @@ callbacks. Write the vendor IDs into `consent-scripts.ts`. See
126
134
  Pass the scripts to the existing `c15tVue` plugin call in `src/main.ts`:
127
135
 
128
136
  ```ts title="src/main.ts"
137
+ import { c15tVue, manifest } from 'c15t/vue/vue-plugin';
129
138
  import { scripts } from './consent-scripts';
130
139
 
131
140
  app.use(c15tVue, {
132
- backendURL: 'https://your-project.inth.app',
141
+ mode: manifest(),
133
142
  scripts,
134
143
  });
135
144
  ```
136
145
 
137
- Keep your existing backend URL and other options. The plugin starts one
146
+ Keep your existing `mode` and other options. The plugin starts one
138
147
  script loader when the app mounts, after it has applied the visitor's stored
139
148
  choice. Do not also call `createScriptLoader` from a component. See
140
149
  [Vue scripts and embeds](https://c15t.com/docs/frameworks/vue/scripts).
@@ -142,12 +151,11 @@ choice. Do not also call `createScriptLoader` from a component. See
142
151
  **Astro**
143
152
 
144
153
  Add the scripts to the client entrypoint from the
145
- [Astro quickstart](https://c15t.com/docs/frameworks/astro/quickstart), the module that the
146
- integration's `clientEntrypoint` option names. Keep `mode`, `ui` and the
147
- framework integration in `astro.config.mjs` as they are. If the module
148
- already exports scripts, combine the two arrays.
154
+ [Astro quickstart](https://c15t.com/docs/frameworks/astro/quickstart), `src/c15t.client.ts`,
155
+ which the integration finds on its own. Keep `astro.config.mjs` as it is.
156
+ If the module already exports scripts, combine the two arrays.
149
157
 
150
- ```ts title="src/consent-client.ts"
158
+ ```ts title="src/c15t.client.ts"
151
159
  import type { C15tClientOptionsExtension } from 'c15t/astro';
152
160
  import { scripts } from './consent-scripts';
153
161
 
@@ -167,15 +175,13 @@ pass them as a top-level prop:
167
175
 
168
176
  ```svelte title="src/App.svelte"
169
177
  <script lang="ts">
170
- import { ConsentManagerProvider, hosted } from '@c15t/svelte';
178
+ import { ConsentProvider, manifest } from '@c15t/svelte';
171
179
  import { scripts } from './consent-scripts';
172
-
173
- const mode = hosted({ url: 'https://your-project.inth.app' });
174
180
  </script>
175
181
 
176
- <ConsentManagerProvider {mode} {scripts}>
182
+ <ConsentProvider mode={manifest()} {scripts}>
177
183
  <!-- Keep your application, consent UI and preferences link here. -->
178
- </ConsentManagerProvider>
184
+ </ConsentProvider>
179
185
  ```
180
186
 
181
187
  Retain the styles and consent UI from the [Svelte quickstart](https://c15t.com/docs/frameworks/svelte/quickstart).
@@ -183,22 +189,21 @@ The provider owns the loader and disposes it on unmount.
183
189
 
184
190
  **SvelteKit**
185
191
 
186
- Add the scripts to the existing root layout provider. Keep the server load
187
- and its serializable prefetch data from the [SvelteKit quickstart](https://c15t.com/docs/frameworks/sveltekit/quickstart).
192
+ Add the scripts to the existing `ConsentRoot` in the root layout. Keep the
193
+ handle and the layout load from the [SvelteKit quickstart](https://c15t.com/docs/frameworks/sveltekit/quickstart).
188
194
 
189
195
  ```svelte title="src/routes/+layout.svelte"
190
196
  <script lang="ts">
191
- import { ConsentManagerProvider, hosted } from '@c15t/svelte';
197
+ import { ConsentRoot } from '@c15t/svelte';
192
198
  import { scripts } from '../consent-scripts';
193
199
 
194
200
  let { children, data } = $props();
195
- const mode = hosted({ url: 'https://your-project.inth.app' });
196
201
  </script>
197
202
 
198
- <ConsentManagerProvider {mode} {scripts} prefetch={data.prefetch}>
203
+ <ConsentRoot state={data.consent} {scripts}>
199
204
  {@render children()}
200
205
  <!-- Keep your consent UI and preferences link here. -->
201
- </ConsentManagerProvider>
206
+ </ConsentRoot>
202
207
  ```
203
208
 
204
209
  Import vendor helpers in the layout component, not in `+layout.server.ts`:
@@ -225,20 +230,19 @@ to a pasted snippet. See [HTML scripts](https://c15t.com/docs/frameworks/html/sc
225
230
 
226
231
  **JavaScript**
227
232
 
228
- Pass the scripts to `init()` from `@c15t/browser`, next to your backend
229
- URL:
233
+ Pass the scripts to `init()` from `@c15t/browser`, next to your mode:
230
234
 
231
235
  ```ts
232
- import { init } from '@c15t/browser';
236
+ import { init, manifest } from '@c15t/browser';
233
237
  import { scripts } from './consent-scripts';
234
238
 
235
239
  const consent = init({
236
- backendURL: 'https://your-project.inth.app',
240
+ mode: manifest(),
237
241
  scripts,
238
242
  });
239
243
  ```
240
244
 
241
- Keep the backend URL from your quickstart. With
245
+ Keep the mode from your quickstart. With
242
246
  `createConsentRuntime` from `c15t/runtime`, pass `scripts` to it instead.
243
247
  A kernel you create yourself needs a loader from
244
248
  `c15t/modules/script-loader`. Attach one loader per kernel. See
@@ -246,25 +250,79 @@ A kernel you create yourself needs a loader from
246
250
 
247
251
  ## Options
248
252
 
249
- | Option | Default | Behavior |
250
- | ----------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------ |
251
- | `id` | Required | Container ID, for example `GTM-1234XXX`. The helper trims it. Empty or whitespace-only values throw. |
252
- | `dataLayer` | `'dataLayer'` | Queue name. A custom name adds `&l=<name>` to the container URL and renames the helper's queue function to `<name>Gtag`. |
253
- | `updateEventName` | `'consent-update'` | `dataLayer` event pushed after every consent update. Use it as a GTM trigger. |
254
- | `consentMapping` | The table below | Replaces the category-to-Google mapping. |
253
+ | Option | Default | Behavior |
254
+ | ----------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
255
+ | `id` | Required | Container ID, for example `GTM-1234XXX`. The helper trims it. Empty or whitespace-only values log an error and the script does not load. |
256
+ | `dataLayer` | `'dataLayer'` | Queue name. A custom name adds `&l=<name>` to the container URL and renames the helper's queue function to `<name>Gtag`. |
257
+ | `updateEventName` | `'consent-update'` | `dataLayer` event pushed after every consent update. Use it as a GTM trigger. |
258
+ | `consentMapping` | The table below | Replaces the category-to-Google mapping. |
259
+ | `loadMode` | `'always'` | When `gtm.js` loads. See [choose when the container loads](#choose-when-the-container-loads). |
260
+ | `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. |
261
+
262
+ ## Choose when the container loads
255
263
 
256
- ## Google loads before a choice
264
+ | `loadMode` | Until the category is allowed | After the category is allowed |
265
+ | ----------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
266
+ | `'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. |
267
+ | `'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`. |
257
268
 
258
- The `googleTagManager` and `gtag` helpers set `alwaysLoad: true`. Before the
259
- visitor chooses, the helper creates the `dataLayer` queue, sends
260
- `gtag('consent', 'default', ...)` with the current permissions and loads
261
- Google's script. After each permission change it sends
269
+ Use `'after-consent'` when your policy forbids any request to Google before
270
+ the visitor opts in:
271
+
272
+ ```ts title="src/consent-scripts.ts (partial)"
273
+ googleTagManager({ id: 'GTM-XXXXXXX', loadMode: 'after-consent' }),
274
+ ```
275
+
276
+ `'after-consent'` waits for the category to be allowed, not for a recorded
277
+ choice. Under an `opt-in` policy, that happens when the visitor allows it.
278
+ Under an `opt-out` or `none` policy, optional categories are allowed before a
279
+ choice, so `gtm.js` loads on the first page unless a saved refusal or a
280
+ privacy signal restricts the category; see
281
+ [policies](../concepts/policies.md).
282
+
283
+ `necessary` is always allowed, so a gated container needs another category.
284
+ This mode defaults to `{ or: ['measurement', 'marketing'] }`. A container
285
+ usually holds Analytics tags and advertising tags, and this default loads it
286
+ once the visitor allows either purpose. Google tags inside still follow the
287
+ Consent Mode types, so an Ads tag stays restricted for a visitor who allowed
288
+ only measurement. If the container holds only Analytics tags, pass
289
+ `category: 'measurement'`.
290
+
291
+ This mode gives up part of Consent Mode. With `'always'`, Google tags send
292
+ cookieless pings while a type is denied, and Google uses them to model
293
+ conversions and behavior for visitors who refused or have not chosen. With
294
+ `'after-consent'`, visitors whose category is denied send nothing, so Google
295
+ has no data to model them from. Reports cover only visitors who allowed the
296
+ category.
297
+
298
+ The container starts with the visitor's current choice in its `default`
299
+ command. On the page where the visitor accepts, it loads after the choice, so
300
+ no `consent-update` event fires for that choice. Triggers that wait only for
301
+ `consent-update` first fire on a later change. Use the tag's consent settings,
302
+ described below, for tags that should fire on the first page.
303
+
304
+ When the visitor withdraws the category, c15t reloads the page and the new
305
+ page does not load `gtm.js`. With `reloadOnConsentRevoked: false`, the running
306
+ container stays on the page and gets an `update` that denies the withdrawn
307
+ types. A later grant reuses it instead of starting a second container.
308
+
309
+ ## Google loads before a choice by default
310
+
311
+ With the default `loadMode: 'always'`, the `googleTagManager` and `gtag`
312
+ helpers set `alwaysLoad: true`. Before the visitor chooses, the helper creates
313
+ the `dataLayer` queue, sends `gtag('consent', 'default', ...)` with the current
314
+ permissions and loads Google's script. After each permission change it sends
262
315
  `gtag('consent', 'update', ...)`. Google's tags then adjust what they store and
263
316
  send; see Google's
264
317
  [Consent Mode overview](https://developers.google.com/tag-platform/security/concepts/consent-mode).
265
318
 
266
- So the browser does contact Google before consent. If your policy requires no
267
- Google request until the visitor allows it, do not use these helpers unchanged.
319
+ So the browser contacts Google before consent. If your policy requires no
320
+ Google request until the visitor allows it, set `loadMode: 'after-consent'`.
321
+ The helper then creates nothing and requests nothing until its category is
322
+ allowed. When it loads, it sends the `default` command first, with the
323
+ permissions at that moment, and `update` commands after later changes. Under
324
+ an `opt-out` or `none` policy, optional categories are allowed before a
325
+ choice, so the helper loads on the first page.
268
326
 
269
327
  ## How categories map to Google consent types
270
328
 
@@ -286,10 +344,12 @@ visitor has chosen anything. That is a permission, not a recorded choice.
286
344
 
287
345
  ## Configure consent inside the container
288
346
 
289
- `googleTagManager` uses the `necessary` category and `alwaysLoad`, so the
290
- container loads on every page. Consent signals reach Google tags that support
291
- Consent Mode. Other tags in the container, such as a third-party pixel added as
292
- Custom HTML, ignore those signals and fire on their triggers.
347
+ With the default `loadMode: 'always'`, `googleTagManager` uses the `necessary`
348
+ category and `alwaysLoad`, so the container loads on every page. With
349
+ `'after-consent'`, it loads once its category is allowed. In both modes,
350
+ consent signals reach Google tags that support Consent Mode. Other tags in the
351
+ container, such as a third-party pixel added as Custom HTML, ignore those
352
+ signals and fire on their triggers.
293
353
 
294
354
  For each non-Google tag, add a consent requirement in the tag's consent
295
355
  settings, or fire it from a Custom Event trigger on `consent-update`. If a
@@ -305,6 +365,8 @@ choices per arm. To see the arm in GTM as well, forward the `onSurfaceShown` and
305
365
 
306
366
  ## Verify Google Tag Manager
307
367
 
368
+ With the default `loadMode: 'always'`:
369
+
308
370
  1. In a private window with an opt-in policy, load the page. `gtm.js` loads.
309
371
  In Google Tag Assistant, the first consent command is `default` with
310
372
  `analytics_storage` and `ad_storage` set to `denied`.
@@ -317,5 +379,19 @@ choices per arm. To see the arm in GTM as well, forward the `onSurfaceShown` and
317
379
  4. Turn measurement off again and save. c15t reloads the page, and the new
318
380
  page starts with `analytics_storage` denied.
319
381
 
382
+ With `loadMode: 'after-consent'`:
383
+
384
+ 1. In a private window with an opt-in policy, filter DevTools Network by
385
+ `google` and load the page. No request appears, and `window.dataLayer` is
386
+ `undefined` in the Console.
387
+ 2. Allow measurement. `gtm.js` loads once. In Tag Assistant, the first command
388
+ is `consent` `default` with `analytics_storage` set to `granted`, before
389
+ the container starts.
390
+ 3. Allow marketing as well. Tag Assistant shows an `update` command that
391
+ grants the ad types, followed by a `consent-update` event. No second
392
+ `gtm.js` request appears.
393
+ 4. Turn both off again and save. c15t reloads the page, and the new page makes
394
+ no request to Google.
395
+
320
396
  See the [consent verification guide](../guides/verify-consent.md) for
321
397
  navigation and hosting checks.