@c15t/react 3.1.0-canary.0 → 3.2.0-canary.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 (129) hide show
  1. package/AGENTS.md +11 -4
  2. package/README.md +4 -4
  3. package/dist/component-hooks/use-consent-gate.js +2 -0
  4. package/dist/component-hooks/use-consent-labels.js +2 -0
  5. package/dist/component-hooks/use-consent-text-direction.js +2 -0
  6. package/dist/component-hooks/use-vendors-listed-under.js +2 -0
  7. package/dist/components/consent-gate/atoms.js +1 -1
  8. package/dist/components/consent-gate/consent-gate.js +1 -1
  9. package/dist/components/iab-prompt/atoms/root.js +1 -1
  10. package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
  11. package/dist/components/preferences/atoms/button.js +1 -1
  12. package/dist/components/preferences/atoms/vendor-list.js +1 -1
  13. package/dist/components/prompt/atoms/root.js +1 -1
  14. package/dist/components/prompt/components.js +1 -1
  15. package/dist/components/prompt/policy-actions.js +1 -1
  16. package/dist/components/prompt/prompt.js +1 -1
  17. package/dist/components/prompt/surface-context.js +1 -1
  18. package/dist/components/prompt/use-banner-copy.js +1 -1
  19. package/dist/components/shared/streamed-surface.js +1 -1
  20. package/dist/event-dispatcher.js +2 -0
  21. package/dist/headless.js +1 -1
  22. package/dist/index-parts/event-dispatcher.js +1 -0
  23. package/dist/index-parts/offline.js +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/modes.js +1 -1
  26. package/dist/provider.js +1 -1
  27. package/dist/transports/offline.js +1 -1
  28. package/dist/version.js +1 -1
  29. package/dist-types/component-hooks/use-consent-gate.d.ts +102 -0
  30. package/dist-types/component-hooks/use-consent-labels.d.ts +32 -0
  31. package/dist-types/component-hooks/use-consent-text-direction.d.ts +22 -0
  32. package/dist-types/component-hooks/use-vendors-listed-under.d.ts +30 -0
  33. package/dist-types/components/prompt/atoms/root.d.ts +3 -3
  34. package/dist-types/components/prompt/prompt.d.ts +4 -3
  35. package/dist-types/components/prompt/surface-context.d.ts +1 -14
  36. package/dist-types/components/prompt/use-banner-copy.d.ts +8 -3
  37. package/dist-types/components/shared/streamed-surface.d.ts +47 -10
  38. package/dist-types/context.d.ts +3 -1
  39. package/dist-types/event-dispatcher.d.ts +24 -0
  40. package/dist-types/headless.d.ts +10 -1
  41. package/dist-types/hooks/use-iab-policy-guard.d.ts +17 -1
  42. package/dist-types/hooks/use-scroll-lock.d.ts +16 -2
  43. package/dist-types/index-parts/event-dispatcher.d.ts +7 -0
  44. package/dist-types/index-parts/offline.d.ts +1 -1
  45. package/dist-types/index.d.ts +2 -0
  46. package/dist-types/modes.d.ts +1 -1
  47. package/dist-types/module-hooks/script-loader.d.ts +9 -0
  48. package/dist-types/provider.d.ts +3 -3
  49. package/dist-types/transports/offline.d.ts +2 -2
  50. package/dist-types/version.d.ts +1 -1
  51. package/docs/README.md +11 -4
  52. package/docs/concepts/choose-your-setup.md +9 -9
  53. package/docs/concepts/consent-state.md +93 -9
  54. package/docs/concepts/data-fetching.md +30 -23
  55. package/docs/concepts/modes.md +85 -35
  56. package/docs/customization/recipes.md +4 -4
  57. package/docs/customization/stylesheets.md +3 -3
  58. package/docs/customization/tailwind.md +17 -34
  59. package/docs/customization/translations.md +18 -12
  60. package/docs/frameworks/react/clear-on-revocation.md +3 -2
  61. package/docs/frameworks/react/components/consent-banner.md +3 -3
  62. package/docs/frameworks/react/components/consent-provider.md +29 -29
  63. package/docs/frameworks/react/gpp.md +1 -1
  64. package/docs/frameworks/react/headless.md +146 -19
  65. package/docs/frameworks/react/iab.md +8 -1
  66. package/docs/frameworks/react/quickstart.md +1 -1
  67. package/docs/frameworks/react/rendering.md +1 -1
  68. package/docs/frameworks/react/scripts.md +44 -0
  69. package/docs/frameworks/react/translations.md +9 -9
  70. package/docs/frameworks/react/troubleshooting.md +1 -1
  71. package/docs/frameworks/react/upgrade-v3.md +39 -32
  72. package/docs/frameworks/react/vendor-consent.md +5 -0
  73. package/docs/guides/banner-experiments.md +3 -3
  74. package/docs/guides/troubleshooting.md +3 -2
  75. package/docs/integrations/adobe-analytics.md +2 -2
  76. package/docs/integrations/ahrefs-analytics.md +2 -2
  77. package/docs/integrations/amplitude.md +2 -2
  78. package/docs/integrations/clay.md +305 -0
  79. package/docs/integrations/clearbit.md +2 -2
  80. package/docs/integrations/cloudflare-web-analytics.md +2 -2
  81. package/docs/integrations/cloudflare-zaraz.md +2 -2
  82. package/docs/integrations/crisp.md +2 -2
  83. package/docs/integrations/databuddy.md +2 -2
  84. package/docs/integrations/fathom-analytics.md +2 -2
  85. package/docs/integrations/front-chat.md +2 -2
  86. package/docs/integrations/google-maps.md +2 -2
  87. package/docs/integrations/google-tag-manager.md +59 -2
  88. package/docs/integrations/google-tag.md +2 -2
  89. package/docs/integrations/heap.md +2 -2
  90. package/docs/integrations/hightouch.md +2 -2
  91. package/docs/integrations/hotjar.md +40 -26
  92. package/docs/integrations/intercom.md +2 -2
  93. package/docs/integrations/klaviyo.md +2 -2
  94. package/docs/integrations/linkedin-insights.md +2 -2
  95. package/docs/integrations/logrocket.md +2 -2
  96. package/docs/integrations/matomo-analytics.md +2 -2
  97. package/docs/integrations/meta-pixel.md +2 -2
  98. package/docs/integrations/microsoft-clarity.md +2 -2
  99. package/docs/integrations/microsoft-uet.md +2 -2
  100. package/docs/integrations/mixpanel-analytics.md +2 -2
  101. package/docs/integrations/one-dollar-stats.md +2 -2
  102. package/docs/integrations/openai-pixel.md +2 -2
  103. package/docs/integrations/other-ads-and-pixels.md +94 -0
  104. package/docs/integrations/other-analytics.md +108 -0
  105. package/docs/integrations/other-chat-and-support.md +112 -0
  106. package/docs/integrations/other-email-and-sms.md +156 -0
  107. package/docs/integrations/other-experience.md +222 -0
  108. package/docs/integrations/overview.md +102 -45
  109. package/docs/integrations/pinterest-tag.md +2 -2
  110. package/docs/integrations/pirsch.md +2 -2
  111. package/docs/integrations/plausible-analytics.md +2 -2
  112. package/docs/integrations/posthog.md +10 -2
  113. package/docs/integrations/promptwatch.md +2 -2
  114. package/docs/integrations/reddit-pixel.md +2 -2
  115. package/docs/integrations/rudderstack.md +2 -2
  116. package/docs/integrations/rybbit-analytics.md +2 -2
  117. package/docs/integrations/segment.md +2 -2
  118. package/docs/integrations/sentry.md +2 -2
  119. package/docs/integrations/shopify.md +323 -0
  120. package/docs/integrations/snapchat-pixel.md +2 -2
  121. package/docs/integrations/tiktok-pixel.md +2 -2
  122. package/docs/integrations/umami-analytics.md +2 -2
  123. package/docs/integrations/vercel-analytics.md +2 -2
  124. package/docs/integrations/vercel-speed-insights.md +7 -2
  125. package/docs/integrations/x-pixel.md +2 -2
  126. package/docs/integrations/youtube.md +2 -2
  127. package/package.json +12 -29
  128. package/dist/components/prompt/resolve-banner-primary-actions.js +0 -1
  129. package/dist-types/components/prompt/resolve-banner-primary-actions.d.ts +0 -14
@@ -31,7 +31,7 @@ fixture. The Next.js 15 and webpack fixtures use the same files. The check
31
31
  confirms that c15t's banner and dialog keep their styles under Tailwind's
32
32
  preflight, that a padding utility on the banner root
33
33
  beats c15t's own padding, and that the `dark:` variant applies. The fixtures run
34
- without a backend, so they use `offline()` mode or a local `backendURL`. Keep the
34
+ without a backend, so they use `local()` mode or a local `backendURL`. Keep the
35
35
  `mode` or `backendURL` from your quickstart.
36
36
 
37
37
  ## Set up Tailwind CSS 4
@@ -165,13 +165,13 @@ when it opens, so the CSS entry holds only Tailwind. Add
165
165
  import svelte from '@astrojs/svelte';
166
166
  import tailwindcss from '@tailwindcss/vite';
167
167
  import { defineConfig } from 'astro/config';
168
- import c15t, { offline } from 'c15t/astro';
168
+ import c15t, { local } from 'c15t/astro';
169
169
 
170
170
  export default defineConfig({
171
171
  integrations: [
172
172
  svelte(),
173
173
  c15t({
174
- mode: offline({
174
+ mode: local({
175
175
  policyRules: [
176
176
  {
177
177
  id: 'tailwind-matrix',
@@ -264,11 +264,10 @@ detects Tailwind 3 and links `c15t/astro/styles.css` itself. Every c15t
264
264
  package that ships a stylesheet exports the plugin as
265
265
  `<package>/postcss-tailwind3`, so use the one from the package you installed:
266
266
 
267
- | You installed | Plugin name |
268
- | --------------------------------------------------------- | --------------------------------- |
269
- | `c15t` (React, Next.js, TanStack Start, Vue, Nuxt, Astro) | `c15t/postcss-tailwind3` |
270
- | `@c15t/svelte` (Svelte, SvelteKit) | `@c15t/svelte/postcss-tailwind3` |
271
- | `@c15t/browser` (script tag in the light DOM) | `@c15t/browser/postcss-tailwind3` |
267
+ | You installed | Plugin name |
268
+ | ---------------------------------------------------------------------------- | --------------------------------- |
269
+ | `c15t` (React, Next.js, TanStack Start, Vue, Nuxt, Astro, Svelte, SvelteKit) | `c15t/postcss-tailwind3` |
270
+ | `@c15t/browser` (script tag in the light DOM) | `@c15t/browser/postcss-tailwind3` |
272
271
 
273
272
  They are the same plugin. A config that already lists
274
273
  `@c15t/ui/postcss-tailwind3` keeps working.
@@ -303,17 +302,9 @@ class. Add the important modifier to utilities you pass to c15t parts:
303
302
  `!bg-red-500`, `hover:!bg-red-500`. A bare `bg-red-500` has no effect. Theme
304
303
  tokens work the same on Tailwind 3 and 4.
305
304
 
306
- In React and Next.js apps, `c15t setup` adds the plugin and the stylesheet
307
- import when it finds Tailwind 3, but does not set `styles: false`. Add that
308
- yourself. It replaces an existing
309
- `styles.tw3.css` import from an earlier setup with `styles.css`. When it
310
- cannot edit your PostCSS config, it prints the step to do by hand. See
311
- [setup](https://c15t.com/docs/cli/commands/setup#set-up-styles-and-tailwind-css).
312
-
313
305
  Create React App ignores PostCSS config files, so Tailwind 3 cannot run the
314
- plugin and the build fails on c15t's stylesheet. `c15t setup` warns
315
- about this. Add the plugin before `tailwindcss` through CRACO, eject, or move
316
- the app to Vite.
306
+ plugin and the build fails on c15t's stylesheet. Add the plugin before
307
+ `tailwindcss` through CRACO, eject, or move the app to Vite.
317
308
 
318
309
  **Next.js**
319
310
 
@@ -521,11 +512,7 @@ stylesheet:
521
512
 
522
513
  ```js title="postcss.config.mjs"
523
514
  export default {
524
- plugins: {
525
- '@c15t/svelte/postcss-tailwind3': {},
526
- tailwindcss: {},
527
- autoprefixer: {},
528
- },
515
+ plugins: { 'c15t/postcss-tailwind3': {}, tailwindcss: {}, autoprefixer: {} },
529
516
  };
530
517
  ```
531
518
 
@@ -540,13 +527,13 @@ export default {
540
527
  } satisfies Config;
541
528
  ```
542
529
 
543
- Import `@c15t/svelte/styles.css` above the directives, import this
530
+ Import `c15t/svelte/styles.css` above the directives, import this
544
531
  stylesheet from `src/main.ts`, and pass `styles={false}` to
545
532
  `ConsentProvider`:
546
533
 
547
534
  ```css title="src/app.css"
548
535
  /* Vite ignores an @import below other rules, so it goes first. */
549
- @import '@c15t/svelte/styles.css';
536
+ @import 'c15t/svelte/styles.css';
550
537
 
551
538
  @tailwind base;
552
539
  @tailwind components;
@@ -557,11 +544,7 @@ stylesheet from `src/main.ts`, and pass `styles={false}` to
557
544
 
558
545
  ```js title="postcss.config.mjs"
559
546
  export default {
560
- plugins: {
561
- '@c15t/svelte/postcss-tailwind3': {},
562
- tailwindcss: {},
563
- autoprefixer: {},
564
- },
547
+ plugins: { 'c15t/postcss-tailwind3': {}, tailwindcss: {}, autoprefixer: {} },
565
548
  };
566
549
  ```
567
550
 
@@ -576,13 +559,13 @@ export default {
576
559
  } satisfies Config;
577
560
  ```
578
561
 
579
- Import `@c15t/svelte/styles.css` above the directives, import this
562
+ Import `c15t/svelte/styles.css` above the directives, import this
580
563
  stylesheet from your root `+layout.svelte`, and pass `styles={false}` to
581
564
  `ConsentRoot`:
582
565
 
583
566
  ```css title="src/app.css"
584
567
  /* Vite ignores an @import below other rules, so it goes first. */
585
- @import '@c15t/svelte/styles.css';
568
+ @import 'c15t/svelte/styles.css';
586
569
 
587
570
  @tailwind base;
588
571
  @tailwind components;
@@ -792,7 +775,7 @@ stylesheet, and the layer order keeps it above preflight:
792
775
 
793
776
  ```html title="light-dom.html"
794
777
  <script
795
- src="/c15t.offline.js"
778
+ src="/c15t.local.js"
796
779
  data-mode="offline"
797
780
  data-country="DE"
798
781
  data-shadow="false"
@@ -814,7 +797,7 @@ instead:
814
797
  window.c15t = [['config', { ui: { shadow: false, styles: false } }]];
815
798
  </script>
816
799
  <script
817
- src="/c15t.offline.js"
800
+ src="/c15t.local.js"
818
801
  data-mode="offline"
819
802
  data-country="DE"
820
803
  defer
@@ -35,7 +35,7 @@ Nuxt module have no `i18n` option; their copy comes from the backend.
35
35
 
36
36
  c15t bundles English. Every other language it ships is a separate entry point
37
37
  in `@c15t/translations`, named by its language code. Importing one registers
38
- c15t's built-in copy for that language. Offline mode can then switch to it,
38
+ c15t's built-in copy for that language. Local mode can then switch to it,
39
39
  and your messages that repeat its wording don't hide backend edits. Import
40
40
  them in the file that configures the provider, so they load in the browser:
41
41
 
@@ -51,9 +51,11 @@ string or build your messages from it:
51
51
  import { translations as de } from '@c15t/translations/de';
52
52
  ```
53
53
 
54
- The entries are `bg`, `cs`, `cy`, `da`, `de`, `el`, `es`, `et`, `fi`, `fr`,
55
- `ga`, `gu`, `he`, `hi`, `hr`, `hu`, `id`, `is`, `it`, `lb`, `lt`, `lv`, `mt`,
56
- `nb`, `nl`, `nn`, `pl`, `pt`, `rm`, `ro`, `sk`, `sl`, `sv` and `zh`.
54
+ The entries are `ar`, `bg`, `bn`, `cs`, `cy`, `da`, `de`, `el`, `es`, `et`,
55
+ `fi`, `fr`, `ga`, `gu`, `he`, `hi`, `hr`, `ht`, `hu`, `id`, `is`, `it`, `ja`,
56
+ `kn`, `ko`, `lb`, `lt`, `lv`, `ml`, `mr`, `ms`, `mt`, `nb`, `nl`, `nn`, `pa`,
57
+ `pl`, `pt`, `rm`, `ro`, `ru`, `sk`, `sl`, `sv`, `ta`, `te`, `th`, `tr`, `ur`,
58
+ `vi`, `yi` and `zh`.
57
59
  `@c15t/translations/all` still loads every language at once. The `c15t`
58
60
  package doesn't re-export these entries, so add `@c15t/translations` to your
59
61
  dependencies.
@@ -102,26 +104,30 @@ production. Rename the section to `consentGate` to remove the warning.
102
104
  ## Show right-to-left languages
103
105
 
104
106
  c15t sets `dir="rtl"` on its surfaces when the resolved language is Arabic,
105
- Hebrew, Persian, Urdu, Pashto, Sindhi, Kurdish or Dhivehi. It matches the
106
- primary language, so `ar-EG` counts as Arabic. Hebrew is the only
107
- right-to-left translation c15t ships. For the others, supply your own
108
- messages or the copy from your Inth project.
107
+ Hebrew, Yiddish, Persian, Urdu, Pashto, Sindhi, Central Kurdish (`ckb`),
108
+ Uyghur, Dhivehi, Syriac or N'Ko. It matches the primary language, so `ar-EG`
109
+ counts as Arabic, and accepts the older codes `iw` and `ji`. A script subtag
110
+ decides over the language: `ku-Arab` and `pa-Arab` are right to left, and
111
+ `sd-Deva` is left to right. Plain `ku` is left to right because it usually
112
+ means Kurmanji, which uses Latin script. c15t ships right-to-left
113
+ translations for Arabic, Hebrew, Urdu and Yiddish. For the others, supply
114
+ your own messages or the copy from your Inth project.
109
115
 
110
116
  What follows the direction:
111
117
 
112
118
  * Text, headings and the button row in the banner, the preference dialog and
113
119
  the preference widget flow right to left.
114
- * A floating or widget banner in its default corner moves to the mirrored
115
- corner, so `bottom-left` becomes `bottom-right`. A `position` you set
116
- yourself stays where you put it.
117
120
  * The HTML script tag, `@c15t/browser` and Astro's banner also set `lang` on
118
121
  their surfaces. React, Vue and Svelte set `dir` only, so set `lang` on
119
122
  `<html>` yourself.
120
123
 
124
+ The banner and the floating `ConsentDialogTrigger` stay in their corners. A
125
+ `bottom-left` banner is still at the bottom left in Hebrew, so it doesn't
126
+ jump across the page when the visitor's language changes.
127
+
121
128
  What does not follow yet:
122
129
 
123
130
  * The IAB TCF dialog aligns several labels, indents and borders to the left.
124
- * The floating `ConsentDialogTrigger` keeps the corner you give it.
125
131
 
126
132
  Test right-to-left pages with a real translation before you ship them.
127
133
 
@@ -62,8 +62,9 @@ replaces the automatic attempts for that field. Partitioned cookies need
62
62
  `partitioned: true`. A prefix only finds cookies the current page can read,
63
63
  so use an exact name for a cookie on another path.
64
64
 
65
- c15t never deletes its own consent records, even when a pattern matches
66
- them.
65
+ c15t never deletes its own consent records or the `c15t-sid` subject ID
66
+ cookie, even when a pattern matches them. The subject ID stays so an
67
+ opt-out can still reach what was kept under it.
67
68
 
68
69
  ## When cleanup runs
69
70
 
@@ -63,7 +63,7 @@ rule with `model: 'none'` owes no rights, so nothing renders unless you add
63
63
  failed, no rule matched and you set no default, or init is still withheld, no
64
64
  consent surface renders: not the banner, the dialog, the widget, the
65
65
  preferences link, or the trigger. They appear as soon as a rule resolves,
66
- without a remount. `offline()` without `policyRules` resolves the recommended
66
+ without a remount. `local()` without `policyRules` resolves the recommended
67
67
  pack, so it shows the strict opt-in banner until you pass a country.
68
68
 
69
69
  Acknowledging a notice records its dismissal. It does not record consent or
@@ -201,8 +201,8 @@ Each variant accepts its own positions:
201
201
  | `widget` | `bottom-left`, `bottom-right`, `top-left`, `top-right` | `bottom-right` |
202
202
  | `wall` | `center` | `center` |
203
203
 
204
- A default corner mirrors left and right for right-to-left languages. A
205
- position you set is never mirrored. A position that is not valid for the
204
+ Right-to-left languages keep the same position; only the text direction
205
+ changes. A position that is not valid for the
206
206
  variant falls back to the default and logs an `invalid-position` diagnostic
207
207
  in development.
208
208
 
@@ -68,35 +68,35 @@ the provider to change them. `scripts`, `vendors`, `networkBlocker`,
68
68
  `iframeBlocker`, `consentCategories`, `overrides`, `user`, `callbacks` and
69
69
  `enabled` apply when they change.
70
70
 
71
- | Option | Purpose |
72
- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
- | `mode` | Required. `hosted()` for the backend's `/init`, `manifest()` for a bundled policy, `offline()` for local policies, all from `c15t/react`, or `custom()` for your own transport. See [consent modes](../../../concepts/modes.md) |
74
- | `prefetch` | Server-resolved state, or a promise of it, from a framework adapter's server helper |
75
- | `scripts` | Consent-gated vendor scripts. See [scripts and embeds](../scripts.md) |
76
- | `vendors` | Vendors listed under their category with their own switch. See [vendor consent](../vendor-consent.md) |
77
- | `scriptLoader` | Options for the script loader module |
78
- | `networkBlocker` | Rules that hold `fetch` and `XMLHttpRequest` calls until their category is allowed, or `false` |
79
- | `iframeBlocker` | Gates DOM iframes that carry `data-category`. On by default; `false` turns it off |
80
- | `persistence` | Stores the visitor's choice in a cookie and localStorage. On by default |
81
- | `storageConfig` | Storage key and cookie settings for the stored choice |
82
- | `clearOnRevocation` | Cookies and storage keys to delete when a category is denied |
83
- | `reloadOnConsentRevoked` | Reload the page after the visitor turns off a granted category or vendor. Defaults to `true` |
84
- | `consentCategories` | Categories the preference dialog offers alongside those your scripts use, within the policy's scope |
85
- | `overrides` | Force the country, region or language the policy is resolved for, for testing |
86
- | `user` | Link consent records to a signed-in user |
87
- | `callbacks` | Choice, permission and error events |
88
- | `i18n` | Languages and message overrides |
89
- | `legalLinks` | Privacy policy and other links shown in the banner and dialog |
90
- | `theme` | `consentActions` and slot styles. Tokens go through `ConsentTheme`; see [customize](../customize.md) |
91
- | `components` | Slot attributes per component part |
92
- | `presentation` | Banner and dialog shape, position and blocking |
93
- | `experiment` | A/B test of `presentation`, read once at mount. See [banner experiments](../../../guides/banner-experiments.md) |
94
- | `journey` | Random id that links each `/init` to the save that follows. See [session reports](../../../concepts/data-fetching.md#link-an-init-to-the-save-that-follows) |
95
- | `colorScheme` | `'light'`, `'dark'`, `'system'`, or `null` to leave the `c15t-dark` class alone |
96
- | `noStyle`, `disableAnimation` | Drop the built-in styles, or turn off animations |
97
- | `preloadDialog` | When the deferred `ConsentDialog` starts loading: `'idle'` (default) or `'intent'`; see [ConsentDialog](./consent-dialog.md#behavior) |
98
- | `nonce` | Content Security Policy nonce for script elements c15t inserts |
99
- | `enabled` | Set `false` only when you deliberately bypass consent enforcement |
71
+ | Option | Purpose |
72
+ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
+ | `mode` | Required. `hosted()` for the backend's `/init`, `manifest()` for a bundled policy, `local()` for local policies, all from `c15t/react`, or `custom()` for your own transport. See [consent modes](../../../concepts/modes.md) |
74
+ | `prefetch` | Server-resolved state, or a promise of it, from a framework adapter's server helper |
75
+ | `scripts` | Consent-gated vendor scripts. See [scripts and embeds](../scripts.md) |
76
+ | `vendors` | Vendors listed under their category with their own switch. See [vendor consent](../vendor-consent.md) |
77
+ | `scriptLoader` | Options for the script loader module |
78
+ | `networkBlocker` | Rules that hold `fetch` and `XMLHttpRequest` calls until their category is allowed, or `false` |
79
+ | `iframeBlocker` | Gates DOM iframes that carry `data-category`. On by default; `false` turns it off |
80
+ | `persistence` | Stores the visitor's choice in a cookie and localStorage. On by default |
81
+ | `storageConfig` | Storage key and cookie settings for the stored choice |
82
+ | `clearOnRevocation` | Cookies and storage keys to delete when a category is denied |
83
+ | `reloadOnConsentRevoked` | Reload the page after the visitor turns off a granted category or vendor. Defaults to `true` |
84
+ | `consentCategories` | Categories the preference dialog offers alongside those your scripts use, within the policy's scope |
85
+ | `overrides` | Force the country, region or language the policy is resolved for, for testing |
86
+ | `user` | Link consent records to a signed-in user |
87
+ | `callbacks` | Choice, permission and error events |
88
+ | `i18n` | Languages and message overrides |
89
+ | `legalLinks` | Privacy policy and other links shown in the banner and dialog |
90
+ | `theme` | `consentActions` and slot styles. Tokens go through `ConsentTheme`; see [customize](../customize.md) |
91
+ | `components` | Slot attributes per component part |
92
+ | `presentation` | Banner and dialog shape, position and blocking |
93
+ | `experiment` | A/B test of `presentation`, read once at mount. See [banner experiments](../../../guides/banner-experiments.md) |
94
+ | `journey` | Random id that links each `/init` to the save that follows. See [session reports](../../../concepts/data-fetching.md#link-an-init-to-the-save-that-follows) |
95
+ | `colorScheme` | `'light'`, `'dark'`, `'system'`, or `null` to leave the `c15t-dark` class alone |
96
+ | `noStyle`, `disableAnimation` | Drop the built-in styles, or turn off animations |
97
+ | `preloadDialog` | When the deferred `ConsentDialog` starts loading: `'idle'` (default) or `'intent'`; see [ConsentDialog](./consent-dialog.md#behavior) |
98
+ | `nonce` | Content Security Policy nonce for script elements c15t inserts |
99
+ | `enabled` | Set `false` only when you deliberately bypass consent enforcement |
100
100
 
101
101
  `enabled: false` grants categories and allows gated loading while suppressing
102
102
  consent UI. It is not a way to fix failed initialization or hide a banner in
@@ -36,7 +36,7 @@ The props are the GPP options listed below, for example
36
36
  pass to `ConsentProvider` as `runtime`.
37
37
 
38
38
  The GPP string follows the policy and location your backend resolves for the
39
- visitor, so a US visitor's state comes from the backend. With `offline()`
39
+ visitor, so a US visitor's state comes from the backend. With `local()`
40
40
  mode, pass the state yourself, for example
41
41
  `overrides: { country: 'US', region: 'CA' }`.
42
42
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Headless
3
- description: Build a custom consent banner in React with the c15t/react/headless
4
- hooks inside your ConsentProvider.
3
+ description: Build a custom consent banner or embed gate in React with the
4
+ c15t/react/headless hooks inside your ConsentProvider.
5
5
  group: frameworks
6
6
  ---
7
7
 
@@ -30,29 +30,28 @@ The headless hooks read the runtime from the nearest `ConsentProvider` from
30
30
  banner, and keep the dialog and persistent preferences control.
31
31
 
32
32
  ```tsx title="src/consent-banner.tsx"
33
- import { useHeadlessConsentUI, useTranslations } from 'c15t/react/headless';
34
-
35
- const ACTION_LABELS = {
36
- accept: 'acceptAll',
37
- customize: 'customize',
38
- dismiss: 'acknowledge',
39
- reject: 'rejectAll',
40
- save: 'save',
41
- } as const;
33
+ import {
34
+ useBannerCopy,
35
+ useConsentLabels,
36
+ useHeadlessConsentUI,
37
+ } from 'c15t/react/headless';
42
38
 
43
39
  export const Banner = () => {
44
40
  const { banner, performAction, openDialog } = useHeadlessConsentUI();
45
- const { common, rights } = useTranslations();
41
+ const { title, description } = useBannerCopy();
42
+ const labels = useConsentLabels();
46
43
 
47
44
  if (!banner.isVisible) {
48
45
  return null;
49
46
  }
50
47
 
51
48
  return (
52
- <section aria-label="Privacy">
49
+ <section aria-label={title}>
50
+ <h2>{title}</h2>
51
+ <p>{description}</p>
53
52
  {banner.preferenceControls.map((right) => (
54
53
  <button key={right} type="button" onClick={openDialog}>
55
- {right === 'opt-out' ? rights?.optOut : rights?.preferences}
54
+ {labels.preferenceControls[right]}
56
55
  </button>
57
56
  ))}
58
57
  {banner.actionGroups.map((group) => (
@@ -64,7 +63,7 @@ export const Banner = () => {
64
63
  data-primary={banner.primaryActions.includes(action) || undefined}
65
64
  onClick={() => performAction(action)}
66
65
  >
67
- {common[ACTION_LABELS[action]]}
66
+ {labels.actions[action]}
68
67
  </button>
69
68
  ))}
70
69
  </div>
@@ -76,8 +75,10 @@ export const Banner = () => {
76
75
 
77
76
  `banner.actionGroups` is the resolved layout: reject and accept share a group
78
77
  at equal prominence, and the rest follow. `banner.orderedActions` is the same
79
- list flattened. Under a notice the only action is `dismiss`, and
80
- `banner.preferenceControls` recommends additional buttons for opening
78
+ list flattened. `banner.primaryActions` lists the actions to style as
79
+ primary. Under a notice the only action is `dismiss`, and
80
+ `banner.primaryActions` contains it. `banner.preferenceControls` recommends
81
+ additional buttons for opening
81
82
  preferences. Under a notice it contains `opt-out`, which selects the
82
83
  "Do not sell or share my data" label. The example renders that button and
83
84
  an "OK" button. Both controls keep their own command: opening
@@ -86,6 +87,20 @@ preferences and dismissing the notice.
86
87
  The list is a rendering helper. It does not establish that your UI implements
87
88
  all policy rights. Provide disclosure and persistent preferences access.
88
89
 
90
+ `useBannerCopy()` returns the `title` and `description` for the active
91
+ prompt. A notice reads `cookieBanner.noticeTitle` and
92
+ `cookieBanner.noticeDescription`, then falls back to the choice copy in the
93
+ same language before English. `useConsentLabels()` returns
94
+ `actions`, a label for each action, and `preferenceControls`, a label for
95
+ each preference control. `dismiss` reads `common.acknowledge`, then
96
+ `common.dismiss`. Both hooks follow the active language and fill missing
97
+ keys from English, and they are the same strings the pre-built banner
98
+ renders.
99
+
100
+ The hooks wrap `resolvePromptCopy`, `resolveActionLabels`, and
101
+ `resolvePreferenceControlLabels`. Those take a translations object and
102
+ are exported from `c15t`, so code outside React can produce the same copy.
103
+
89
104
  `performAction` saves all categories for `accept`, none for `reject`, the
90
105
  current draft for `save`, records a dismissal for `dismiss`, and opens the
91
106
  preference center for `customize`. `banner.diagnostics` reports when a host
@@ -94,5 +109,117 @@ prominence. Review each diagnostic when configuring custom presentation.
94
109
 
95
110
  `banner.variant`, `banner.position`, and `banner.blocking` carry the resolved
96
111
  shape from `presentation.prompt`, so a headless surface can follow the same
97
- bar, widget, or wall choice the pre-built banner would make, and can trap
98
- focus and lock scroll exactly when `blocking` is true.
112
+ bar, widget, or wall choice the pre-built banner would make. The pre-built
113
+ banner traps focus and locks page scroll while `blocking` is true. To do
114
+ the same, call `useFocusTrap(blocking, ref)` and `useScrollLock(blocking)`
115
+ from the headless entry, with `blocking` set to
116
+ `banner.isVisible && banner.blocking` and `ref` attached to your banner's
117
+ root element. Scroll locks are counted, so the page scrolls again once the
118
+ banner and any open dialog have both let go.
119
+
120
+ c15t can show consent copy in the visitor's browser language, which may run
121
+ right to left on a left-to-right page. `useConsentTextDirection()` returns
122
+ `'rtl'` or `'ltr'` for the language the copy is in. Set it as `dir` on your
123
+ banner and preference center roots, as the pre-built ones do.
124
+
125
+ The pre-built banner and dialog render nothing under the IAB model, and
126
+ the TCF surfaces inside `<IABProvider>` take over. A
127
+ custom surface that handles only opt-in and opt-out should do the same:
128
+ return `null` when `useModel()` is `'iab'`. Call `useIABPolicyGuard()` in
129
+ it as well. It throws `IABUnavailableError` while rendering when the
130
+ policy needs IAB and the app never loaded `<IABProvider>`, so the gap
131
+ shows up in your error boundary instead of as a missing banner.
132
+
133
+ ## Build a custom consent gate
134
+
135
+ `useConsentGate(category)` tells a custom gate what to render for one
136
+ consent category, with translated copy and an action that opens the
137
+ preference center. Use it when the stock `ConsentGate` placeholder does not
138
+ fit your design, for example a design system card in front of a video or
139
+ map embed. `ConsentGate` runs on the same hook, so both gates follow the
140
+ same rules.
141
+
142
+ This gate renders its children once `marketing` is granted and the page has
143
+ hydrated, the policy message when the policy blocks `marketing`, and
144
+ otherwise a placeholder with a button that opens the preference center:
145
+
146
+ ```tsx title="src/video-gate.tsx"
147
+ import { useConsentGate } from 'c15t/react/headless';
148
+ import type { ReactNode } from 'react';
149
+
150
+ export const VideoGate = ({ children }: { children: ReactNode }) => {
151
+ const { state, title, actionLabel, policyBlockedMessage, requestConsent } =
152
+ useConsentGate('marketing');
153
+
154
+ if (state === 'granted') {
155
+ return children;
156
+ }
157
+ if (state === 'pending') {
158
+ return null;
159
+ }
160
+ if (state === 'policy-blocked') {
161
+ return <p>{policyBlockedMessage}</p>;
162
+ }
163
+ return (
164
+ <div>
165
+ <p>{title}</p>
166
+ <button type="button" onClick={requestConsent}>
167
+ {actionLabel}
168
+ </button>
169
+ </div>
170
+ );
171
+ };
172
+ ```
173
+
174
+ Render `VideoGate` inside your `ConsentProvider` around the embed, such as a
175
+ YouTube `iframe`.
176
+
177
+ | Field | Type | Description |
178
+ | ---------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
179
+ | `state` | `ConsentGateState` | `'granted'`, `'pending'`, `'denied'` or `'policy-blocked'`. |
180
+ | `categoryTitle` | `string` | `consentTypes.<category>.title` from your translations, or the category name when they have none. |
181
+ | `title` | `string` or `undefined` | `consentGate.title`, `Accept {category} consent to view this content.`, with `{category}` replaced by `categoryTitle`. |
182
+ | `actionLabel` | `string` or `undefined` | `consentGate.actionButton`, `Enable {category} consent`, with `{category}` replaced by `categoryTitle`. |
183
+ | `policyBlockedMessage` | `string` | `consentGate.policyBlocked`, or the English message when your translations have none. |
184
+ | `requestConsent` | `() => void` | Opens the preference center. |
185
+
186
+ The result type is exported as `UseConsentGateResult` and the state type as
187
+ `ConsentGateState`. Render from `state`:
188
+
189
+ * `'granted'`: the effective permission for the category is granted and
190
+ the page has hydrated. Render the content.
191
+ * `'pending'`: permission is granted but the page has not hydrated yet.
192
+ Render nothing.
193
+ * `'denied'`: permission is not granted. Render a placeholder with a
194
+ button that calls `requestConsent`.
195
+ * `'policy-blocked'`: the policy has `scopeMode: 'strict'` and its scope
196
+ leaves the category out, so the visitor cannot grant it. Render
197
+ `policyBlockedMessage` without a button. `necessary` is never blocked.
198
+
199
+ Permission is the value `useConsent(category)` returns. An opt-out policy
200
+ can grant it before the visitor chooses, and a privacy signal such as GPC
201
+ can withdraw a stored grant.
202
+
203
+ Granted content never appears in the server HTML. When the server snapshot
204
+ grants the category, `state` is `'pending'` in the server HTML and during
205
+ hydration, then turns `'granted'`, so an iframe inside a streamed
206
+ `Suspense` boundary loads once. Server rendering and hydration evaluate the
207
+ snapshot at its own evaluation time and never read the clock, so the gate
208
+ works in a statically prerendered page.
209
+
210
+ `requestConsent` does not grant the category, because the visitor still has
211
+ to save. The preference center opens on its usual view, not on the
212
+ category. Mount a `ConsentDialog` in the same provider so it has something
213
+ to open. The stock gate button starts loading the pre-built dialog's code
214
+ on hover, focus and idle time. `requestConsent` doesn't, so without a
215
+ banner or dialog trigger on the page, the first open waits for that
216
+ download.
217
+
218
+ When it mounts, the hook registers the category with the provider, as
219
+ `ConsentGate` does, so the preference center can list it. If you set
220
+ `options.consentCategories` on the provider, list the category there too,
221
+ so the preference center shows it before a gate mounts, including on pages
222
+ without one.
223
+
224
+ The hook renders no wrapper element. Reserve the embed's space in your own
225
+ markup so the page does not shift when the placeholder is replaced.
@@ -274,7 +274,7 @@ The backend's `additionalConsent` option sets the cache, which defaults to the
274
274
 
275
275
  ### List the providers in the browser
276
276
 
277
- Offline mode has no backend, and some backends do not list providers. In
277
+ Local mode has no backend, and some backends do not list providers. In
278
278
  those setups, pass `additionalConsent` with your other IAB options: the
279
279
  `IABProvider` props in React, Next.js and TanStack Start, the `createIAB`
280
280
  options in JavaScript, and the `iab` option of the Vue, Nuxt, Svelte, Astro
@@ -380,6 +380,13 @@ its own CMP ID. It lists the CMPs it has certified on its
380
380
  Ask Google to list your CMP ID through its CMP intake form before you rely on
381
381
  the AC string in production.
382
382
 
383
+ ## c15t's IAB CMP registration
384
+
385
+ Inth makes c15t. Inth's legal entity, Consent Management Inc., is registered
386
+ with IAB Europe as a CMP under CMP ID 490. The
387
+ [IAB Europe CMP list](https://iabeurope.eu/cmp-list/) shows it under the name
388
+ Consent Management Inc.
389
+
383
390
  ## How the preference centre shows features
384
391
 
385
392
  The stock IAB preference centres in React, Next.js, Vue, Svelte, Astro and the
@@ -25,7 +25,7 @@ You need an [Inth](https://inth.com) project. In the project, set the policy
25
25
  rules for your regions, add your site's origin to the trusted origins, and copy
26
26
  the backend URL.
27
27
  [Choose your setup](../../concepts/choose-your-setup.md#decide-who-runs-the-consent-backend)
28
- covers self-hosting and offline mode.
28
+ covers self-hosting and local mode.
29
29
 
30
30
  The guide uses `hosted()` mode. Policy, translation and vendor edits in your
31
31
  project reach visitors on their next page load, without a rebuild.
@@ -54,7 +54,7 @@ usually shows 50 to 250 ms sooner. There is nothing to configure.
54
54
 
55
55
  The early request can't read your app's options, so in a few setups it won't
56
56
  match and the app sends a second `/init`. Pass `initPrefetch: false` when the
57
- app uses `offline()` or gives `hosted()` its own `backendURL`, `fetch`,
57
+ app uses `local()` or gives `hosted()` its own `backendURL`, `fetch`,
58
58
  `initURL` or `headers`. With its own `fetch`, `hosted()` sends `/init` through
59
59
  it and ignores the early response. If it sets `credentials`, `overrides`, `storageKey`, an
60
60
  `experiment`, or `journey: false`, pass the same values as
@@ -102,6 +102,50 @@ their vendor slug. See [vendor consent](./vendor-consent.md).
102
102
  To send your own events only to allowed integrations, see
103
103
  [send events only to allowed integrations](../../integrations/overview.md#send-events-only-to-allowed-integrations).
104
104
 
105
+ ## Send your own analytics events
106
+
107
+ `useEventDispatcher()` returns the provider's event dispatcher. It sends to the
108
+ `scripts` you registered and checks consent when an event goes out. Call
109
+ `pageview` from your router with the current path:
110
+
111
+ ```tsx title="src/analytics-events.tsx"
112
+ import { useEventDispatcher } from 'c15t/react';
113
+ import { useEffect } from 'react';
114
+
115
+ /**
116
+ * Sends a PostHog pageview for each new path. Call it once, inside
117
+ * `ConsentProvider`, with your router's current path, such as
118
+ * `useLocation().pathname` from React Router.
119
+ */
120
+ export const usePageviews = (path: string) => {
121
+ const events = useEventDispatcher({ pageviews: ['posthog'] });
122
+ useEffect(() => {
123
+ events.pageview(path);
124
+ }, [events, path]);
125
+ };
126
+
127
+ export const SearchButton = ({ resultCount }: { resultCount: number }) => {
128
+ const events = useEventDispatcher();
129
+ return (
130
+ <button
131
+ type="button"
132
+ onClick={() => {
133
+ events.track('search', { resultCount });
134
+ }}
135
+ >
136
+ Search
137
+ </button>
138
+ );
139
+ };
140
+ ```
141
+
142
+ `track` sends to every registered script with an event API and drops the
143
+ event while measurement is denied. `pageview` sends only to the integrations
144
+ named in `pageviews`, and the first call records the path without sending.
145
+ Every dispatcher shares one pageview history for the provider's lifetime.
146
+ See [send events only to allowed integrations](../../integrations/overview.md#send-events-only-to-allowed-integrations)
147
+ for the supported vendors.
148
+
105
149
  ## Check the scripts
106
150
 
107
151
  Open DevTools Network in a private window, filter by each vendor's domain and