@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.
- package/AGENTS.md +11 -4
- package/README.md +4 -4
- package/dist/component-hooks/use-consent-gate.js +2 -0
- package/dist/component-hooks/use-consent-labels.js +2 -0
- package/dist/component-hooks/use-consent-text-direction.js +2 -0
- package/dist/component-hooks/use-vendors-listed-under.js +2 -0
- package/dist/components/consent-gate/atoms.js +1 -1
- package/dist/components/consent-gate/consent-gate.js +1 -1
- package/dist/components/iab-prompt/atoms/root.js +1 -1
- package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
- package/dist/components/preferences/atoms/button.js +1 -1
- package/dist/components/preferences/atoms/vendor-list.js +1 -1
- package/dist/components/prompt/atoms/root.js +1 -1
- package/dist/components/prompt/components.js +1 -1
- package/dist/components/prompt/policy-actions.js +1 -1
- package/dist/components/prompt/prompt.js +1 -1
- package/dist/components/prompt/surface-context.js +1 -1
- package/dist/components/prompt/use-banner-copy.js +1 -1
- package/dist/components/shared/streamed-surface.js +1 -1
- package/dist/event-dispatcher.js +2 -0
- package/dist/headless.js +1 -1
- package/dist/index-parts/event-dispatcher.js +1 -0
- package/dist/index-parts/offline.js +1 -1
- package/dist/index.js +1 -1
- package/dist/modes.js +1 -1
- package/dist/provider.js +1 -1
- package/dist/transports/offline.js +1 -1
- package/dist/version.js +1 -1
- package/dist-types/component-hooks/use-consent-gate.d.ts +102 -0
- package/dist-types/component-hooks/use-consent-labels.d.ts +32 -0
- package/dist-types/component-hooks/use-consent-text-direction.d.ts +22 -0
- package/dist-types/component-hooks/use-vendors-listed-under.d.ts +30 -0
- package/dist-types/components/prompt/atoms/root.d.ts +3 -3
- package/dist-types/components/prompt/prompt.d.ts +4 -3
- package/dist-types/components/prompt/surface-context.d.ts +1 -14
- package/dist-types/components/prompt/use-banner-copy.d.ts +8 -3
- package/dist-types/components/shared/streamed-surface.d.ts +47 -10
- package/dist-types/context.d.ts +3 -1
- package/dist-types/event-dispatcher.d.ts +24 -0
- package/dist-types/headless.d.ts +10 -1
- package/dist-types/hooks/use-iab-policy-guard.d.ts +17 -1
- package/dist-types/hooks/use-scroll-lock.d.ts +16 -2
- package/dist-types/index-parts/event-dispatcher.d.ts +7 -0
- package/dist-types/index-parts/offline.d.ts +1 -1
- package/dist-types/index.d.ts +2 -0
- package/dist-types/modes.d.ts +1 -1
- package/dist-types/module-hooks/script-loader.d.ts +9 -0
- package/dist-types/provider.d.ts +3 -3
- package/dist-types/transports/offline.d.ts +2 -2
- package/dist-types/version.d.ts +1 -1
- package/docs/README.md +11 -4
- package/docs/concepts/choose-your-setup.md +9 -9
- package/docs/concepts/consent-state.md +93 -9
- package/docs/concepts/data-fetching.md +30 -23
- package/docs/concepts/modes.md +85 -35
- package/docs/customization/recipes.md +4 -4
- package/docs/customization/stylesheets.md +3 -3
- package/docs/customization/tailwind.md +17 -34
- package/docs/customization/translations.md +18 -12
- package/docs/frameworks/react/clear-on-revocation.md +3 -2
- package/docs/frameworks/react/components/consent-banner.md +3 -3
- package/docs/frameworks/react/components/consent-provider.md +29 -29
- package/docs/frameworks/react/gpp.md +1 -1
- package/docs/frameworks/react/headless.md +146 -19
- package/docs/frameworks/react/iab.md +8 -1
- package/docs/frameworks/react/quickstart.md +1 -1
- package/docs/frameworks/react/rendering.md +1 -1
- package/docs/frameworks/react/scripts.md +44 -0
- package/docs/frameworks/react/translations.md +9 -9
- package/docs/frameworks/react/troubleshooting.md +1 -1
- package/docs/frameworks/react/upgrade-v3.md +39 -32
- package/docs/frameworks/react/vendor-consent.md +5 -0
- package/docs/guides/banner-experiments.md +3 -3
- package/docs/guides/troubleshooting.md +3 -2
- package/docs/integrations/adobe-analytics.md +2 -2
- package/docs/integrations/ahrefs-analytics.md +2 -2
- package/docs/integrations/amplitude.md +2 -2
- package/docs/integrations/clay.md +305 -0
- package/docs/integrations/clearbit.md +2 -2
- package/docs/integrations/cloudflare-web-analytics.md +2 -2
- package/docs/integrations/cloudflare-zaraz.md +2 -2
- package/docs/integrations/crisp.md +2 -2
- package/docs/integrations/databuddy.md +2 -2
- package/docs/integrations/fathom-analytics.md +2 -2
- package/docs/integrations/front-chat.md +2 -2
- package/docs/integrations/google-maps.md +2 -2
- package/docs/integrations/google-tag-manager.md +59 -2
- package/docs/integrations/google-tag.md +2 -2
- package/docs/integrations/heap.md +2 -2
- package/docs/integrations/hightouch.md +2 -2
- package/docs/integrations/hotjar.md +40 -26
- package/docs/integrations/intercom.md +2 -2
- package/docs/integrations/klaviyo.md +2 -2
- package/docs/integrations/linkedin-insights.md +2 -2
- package/docs/integrations/logrocket.md +2 -2
- package/docs/integrations/matomo-analytics.md +2 -2
- package/docs/integrations/meta-pixel.md +2 -2
- package/docs/integrations/microsoft-clarity.md +2 -2
- package/docs/integrations/microsoft-uet.md +2 -2
- package/docs/integrations/mixpanel-analytics.md +2 -2
- package/docs/integrations/one-dollar-stats.md +2 -2
- package/docs/integrations/openai-pixel.md +2 -2
- package/docs/integrations/other-ads-and-pixels.md +94 -0
- package/docs/integrations/other-analytics.md +108 -0
- package/docs/integrations/other-chat-and-support.md +112 -0
- package/docs/integrations/other-email-and-sms.md +156 -0
- package/docs/integrations/other-experience.md +222 -0
- package/docs/integrations/overview.md +102 -45
- package/docs/integrations/pinterest-tag.md +2 -2
- package/docs/integrations/pirsch.md +2 -2
- package/docs/integrations/plausible-analytics.md +2 -2
- package/docs/integrations/posthog.md +10 -2
- package/docs/integrations/promptwatch.md +2 -2
- package/docs/integrations/reddit-pixel.md +2 -2
- package/docs/integrations/rudderstack.md +2 -2
- package/docs/integrations/rybbit-analytics.md +2 -2
- package/docs/integrations/segment.md +2 -2
- package/docs/integrations/sentry.md +2 -2
- package/docs/integrations/shopify.md +323 -0
- package/docs/integrations/snapchat-pixel.md +2 -2
- package/docs/integrations/tiktok-pixel.md +2 -2
- package/docs/integrations/umami-analytics.md +2 -2
- package/docs/integrations/vercel-analytics.md +2 -2
- package/docs/integrations/vercel-speed-insights.md +7 -2
- package/docs/integrations/x-pixel.md +2 -2
- package/docs/integrations/youtube.md +2 -2
- package/package.json +12 -29
- package/dist/components/prompt/resolve-banner-primary-actions.js +0 -1
- 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 `
|
|
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, {
|
|
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:
|
|
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
|
|
268
|
-
|
|
|
269
|
-
| `c15t` (React, Next.js, TanStack Start, Vue, Nuxt, Astro) | `c15t/postcss-tailwind3` |
|
|
270
|
-
| `@c15t/
|
|
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.
|
|
315
|
-
|
|
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
|
|
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 '
|
|
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
|
|
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 '
|
|
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.
|
|
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.
|
|
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.
|
|
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`,
|
|
55
|
-
`ga`, `gu`, `he`, `hi`, `hr`, `hu`, `id`, `is`, `it`, `
|
|
56
|
-
`
|
|
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
|
|
106
|
-
primary language, so `ar-EG`
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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. `
|
|
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
|
-
|
|
205
|
-
|
|
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, `
|
|
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 `
|
|
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
|
|
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 {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 {
|
|
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=
|
|
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
|
|
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
|
-
{
|
|
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.
|
|
80
|
-
|
|
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
|
|
98
|
-
focus and
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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
|