@c15t/react 3.0.0-alpha.2 → 3.0.0-alpha.3

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 (148) hide show
  1. package/AGENTS.md +6 -1
  2. package/dist/aggregate-components.js +1 -1
  3. package/dist/chunk-warming.js +1 -1
  4. package/dist/component-hooks/use-headless-iab-consent-ui.js +1 -1
  5. package/dist/component-hooks/use-panel-trigger.js +1 -1
  6. package/dist/components/consent-gate/consent-gate.js +1 -1
  7. package/dist/components/consent-preferences-link/consent-preferences-link.js +1 -1
  8. package/dist/components/iab-panel/atoms/purpose-item.js +1 -1
  9. package/dist/components/iab-panel/atoms/stack-item.js +1 -1
  10. package/dist/components/iab-panel/iab-panel.js +1 -1
  11. package/dist/components/iab-panel/index.js +1 -1
  12. package/dist/components/panel/atoms/overlay.js +1 -1
  13. package/dist/components/panel/atoms/root.js +1 -1
  14. package/dist/components/panel/index.js +1 -1
  15. package/dist/components/panel/panel.js +1 -1
  16. package/dist/components/panel-trigger/atoms/button.js +1 -1
  17. package/dist/components/panel-trigger/atoms/icon.js +1 -1
  18. package/dist/components/panel-trigger/atoms/root.js +1 -1
  19. package/dist/components/panel-trigger/atoms/trigger-icons.js +1 -0
  20. package/dist/components/panel-trigger/panel-trigger-toolbar.js +1 -1
  21. package/dist/components/preferences/atoms/accordion.js +1 -1
  22. package/dist/components/preferences/atoms/root.js +1 -1
  23. package/dist/components/preferences/atoms/vendor-list.js +1 -1
  24. package/dist/components/preferences/index.js +1 -1
  25. package/dist/components/prompt/policy-actions.js +1 -1
  26. package/dist/components/shared/primitives/button.js +1 -1
  27. package/dist/components/shared/ui/accordion/accordion.js +1 -1
  28. package/dist/components/shared/ui/collapsible/collapsible.js +1 -1
  29. package/dist/components/shared/ui/logo.js +1 -1
  30. package/dist/components/shared/ui/preference-item/preference-item.js +1 -1
  31. package/dist/components/shared/ui/switch/switch.js +1 -1
  32. package/dist/components/shared/ui/tabs/tabs.js +1 -1
  33. package/dist/consent-theme.js +1 -0
  34. package/dist/draft.js +1 -1
  35. package/dist/external-iab-context.js +1 -1
  36. package/dist/headless.js +1 -1
  37. package/dist/hooks/use-committed-ref.js +2 -0
  38. package/dist/hooks.js +1 -1
  39. package/dist/iab/styles.tw3.css +1 -41
  40. package/dist/iab-context.js +1 -1
  41. package/dist/index.js +1 -1
  42. package/dist/kernel-selector.js +1 -0
  43. package/dist/module-hooks/iframe-blocker.js +1 -1
  44. package/dist/module-hooks/network-blocker.js +1 -1
  45. package/dist/module-hooks/network-hold.js +2 -0
  46. package/dist/module-hooks/persistence.js +1 -1
  47. package/dist/panel.js +1 -1
  48. package/dist/preferences.js +1 -2
  49. package/dist/provider.js +1 -1
  50. package/dist/styles.tw3.css +0 -45
  51. package/dist/ui-save.js +1 -1
  52. package/dist/utils/deferred-module.js +1 -1
  53. package/dist/version.js +1 -1
  54. package/dist-types/chunk-warming.d.ts +41 -8
  55. package/dist-types/component-hooks/use-headless-iab-consent-ui.d.ts +1 -1
  56. package/dist-types/components/iab-panel/atoms/purpose-item.d.ts +4 -1
  57. package/dist-types/components/iab-panel/atoms/stack-item.d.ts +6 -1
  58. package/dist-types/components/iab-panel/index.d.ts +1 -0
  59. package/dist-types/components/panel/index.d.ts +1 -0
  60. package/dist-types/components/panel/panel.d.ts +1 -1
  61. package/dist-types/components/panel-trigger/atoms/trigger-icons.d.ts +20 -0
  62. package/dist-types/components/preferences/index.d.ts +1 -0
  63. package/dist-types/components/shared/ui/accordion/accordion.d.ts +1 -0
  64. package/dist-types/components/shared/ui/collapsible/collapsible.d.ts +1 -0
  65. package/dist-types/components/shared/ui/logo.d.ts +0 -10
  66. package/dist-types/components/shared/ui/preference-item/preference-item.d.ts +14 -0
  67. package/dist-types/components/shared/ui/switch/switch.d.ts +1 -0
  68. package/dist-types/components/shared/ui/tabs/tabs.d.ts +1 -0
  69. package/dist-types/consent-theme.d.ts +38 -0
  70. package/dist-types/draft.d.ts +43 -6
  71. package/dist-types/headless.d.ts +0 -1
  72. package/dist-types/hooks/use-committed-ref.d.ts +24 -0
  73. package/dist-types/hooks.d.ts +2 -0
  74. package/dist-types/index.d.ts +3 -2
  75. package/dist-types/kernel-selector.d.ts +66 -0
  76. package/dist-types/module-hooks/iframe-blocker.d.ts +27 -0
  77. package/dist-types/module-hooks/network-blocker.d.ts +23 -0
  78. package/dist-types/module-hooks/network-hold.d.ts +47 -0
  79. package/dist-types/panel.d.ts +13 -1
  80. package/dist-types/preferences.d.ts +12 -1
  81. package/dist-types/provider.d.ts +41 -8
  82. package/dist-types/ui-config-context.d.ts +2 -0
  83. package/dist-types/ui-save.d.ts +21 -1
  84. package/dist-types/utils/deferred-module.d.ts +3 -1
  85. package/dist-types/version.d.ts +1 -1
  86. package/docs/README.md +6 -1
  87. package/docs/customization/overview.md +4 -3
  88. package/docs/customization/recipes.md +4 -2
  89. package/docs/customization/tokens.md +66 -3
  90. package/docs/frameworks/react/components/consent-dialog.md +45 -6
  91. package/docs/frameworks/react/components/consent-gate.md +8 -4
  92. package/docs/frameworks/react/components/consent-manager-provider.md +34 -14
  93. package/docs/frameworks/react/components/consent-widget.md +9 -0
  94. package/docs/frameworks/react/concepts/consent-categories.md +8 -1
  95. package/docs/frameworks/react/hooks/{use-consent-manager/overview.md → overview.md} +7 -5
  96. package/docs/frameworks/react/iab/overview.md +121 -1
  97. package/docs/frameworks/react/network-blocker.md +146 -0
  98. package/docs/frameworks/react/script-loader.md +6 -0
  99. package/docs/frameworks/react/styling/overview.md +71 -22
  100. package/docs/guides/consent-state.md +327 -0
  101. package/docs/guides/shared-consent-controls.md +158 -0
  102. package/docs/integrations/adobe-analytics.md +1 -1
  103. package/docs/integrations/ahrefs-analytics.md +1 -1
  104. package/docs/integrations/amplitude.md +1 -1
  105. package/docs/integrations/clearbit.md +1 -1
  106. package/docs/integrations/cloudflare-web-analytics.md +1 -1
  107. package/docs/integrations/cloudflare-zaraz.md +1 -1
  108. package/docs/integrations/crisp.md +1 -1
  109. package/docs/integrations/databuddy.md +1 -1
  110. package/docs/integrations/fathom-analytics.md +1 -1
  111. package/docs/integrations/front-chat.md +322 -0
  112. package/docs/integrations/google-maps.md +1 -1
  113. package/docs/integrations/google-tag-manager.md +1 -1
  114. package/docs/integrations/google-tag.md +1 -1
  115. package/docs/integrations/granular-consent.md +3 -1
  116. package/docs/integrations/heap.md +1 -1
  117. package/docs/integrations/hightouch.md +1 -1
  118. package/docs/integrations/hotjar.md +1 -1
  119. package/docs/integrations/intercom.md +1 -1
  120. package/docs/integrations/linkedin-insights.md +1 -1
  121. package/docs/integrations/logrocket.md +1 -1
  122. package/docs/integrations/matomo-analytics.md +1 -1
  123. package/docs/integrations/meta-pixel.md +1 -1
  124. package/docs/integrations/microsoft-clarity.md +1 -1
  125. package/docs/integrations/microsoft-uet.md +1 -1
  126. package/docs/integrations/mixpanel-analytics.md +1 -1
  127. package/docs/integrations/one-dollar-stats.md +305 -0
  128. package/docs/integrations/openai-pixel.md +1 -1
  129. package/docs/integrations/overview.md +17 -14
  130. package/docs/integrations/pinterest-tag.md +321 -0
  131. package/docs/integrations/pirsch.md +1 -1
  132. package/docs/integrations/plausible-analytics.md +1 -1
  133. package/docs/integrations/posthog.md +1 -1
  134. package/docs/integrations/promptwatch.md +1 -1
  135. package/docs/integrations/reddit-pixel.md +1 -1
  136. package/docs/integrations/rudderstack.md +1 -1
  137. package/docs/integrations/rybbit-analytics.md +1 -1
  138. package/docs/integrations/segment.md +1 -1
  139. package/docs/integrations/snapchat-pixel.md +1 -1
  140. package/docs/integrations/tiktok-pixel.md +1 -1
  141. package/docs/integrations/umami-analytics.md +1 -1
  142. package/docs/integrations/vercel-analytics.md +1 -1
  143. package/docs/integrations/x-pixel.md +1 -1
  144. package/docs/integrations/youtube.md +1 -1
  145. package/docs/upgrade-v3.md +129 -1
  146. package/package.json +7 -7
  147. package/dist/component-hooks/use-manager.js +0 -2
  148. package/dist-types/component-hooks/use-manager.d.ts +0 -45
@@ -11,10 +11,53 @@ React, Next.js and Svelte provide `styles.css`. Import the adapter's stylesheet
11
11
  at the app's global entry point. Vue includes styles in its components; Astro
12
12
  adds styles through its integration.
13
13
 
14
+ The stylesheet blocks rendering, so it carries only what a first paint can
15
+ show: the default tokens, every c15t CSS variable, and the rules for the
16
+ banner, `ConsentDialogTrigger` and the `ConsentGate` placeholder. The consent
17
+ dialog and preference widget bring their own rules. Each rule reaches the page
18
+ once.
19
+
14
20
  ```tsx
15
21
  import 'c15t/react/styles.css';
16
22
  ```
17
23
 
24
+ | File | Holds | Loaded by |
25
+ | -------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------- |
26
+ | `styles.css` or `styles.tw3.css` | Default tokens, all variables, banner, trigger and `ConsentGate` rules | Your app, once |
27
+ | `@c15t/ui/styles/dialog.css` | Dialog and preference widget rules | The dialog component, with its lazy chunk |
28
+ | `@c15t/ui/styles/primitives.css` | Rules for the `@c15t/ui/styles/primitives` class maps | Svelte's `styles.css`, or your app if it renders those class maps |
29
+ | `iab/styles.css` | IAB TCF banner and dialog rules and variables | Your app, after `styles.css` |
30
+
31
+ In React, Next.js and TanStack Start, the dialog's module imports
32
+ `@c15t/ui/styles/dialog.css`. The bundler emits it with the dialog's chunk and
33
+ loads it before that chunk runs, so the dialog never renders unstyled. The
34
+ chunk loads after first paint, by the time the dialog first opens. Do not
35
+ import `dialog.css` yourself. The `@c15t/react/components/consent-dialog`,
36
+ `/components/consent-widget`, `/primitives`, `/primitives/*` and `/iab` entry
37
+ points import it as soon as you import them, because they render dialog parts
38
+ outside the lazy chunk.
39
+
40
+ These modules import the stylesheet through `@c15t/ui/styles/dialog`. Under
41
+ the `node` export condition that module imports nothing, so server code that
42
+ loads `@c15t/react` with plain Node, such as the Pages Router or an SSR build
43
+ that keeps dependencies external, does not fail on the `.css` file. In your
44
+ own components, import `@c15t/ui/styles/dialog` rather than the `.css` file
45
+ if the module can run on the server.
46
+
47
+ Svelte loads its dialog with the page, so `c15t/svelte/styles.css` also
48
+ imports the dialog and primitive rules. Astro injects the banner rules; the
49
+ React and Svelte dialog islands import the rest, and Astro links those
50
+ stylesheets on every page, so in Astro the dialog rules still block rendering.
51
+ Vue components import their own stylesheets.
52
+
53
+ The dialog rules sit in `@layer components` and declare no variables, so
54
+ tokens from `theme`, variables you override in your own CSS, and Tailwind 4
55
+ utilities still win over them even though they load later. If you import
56
+ `styles.css` into a named layer, such as `@import 'c15t/react/styles.css'
57
+ layer(c15t)`, the dialog rules still join the top-level `components` layer.
58
+ List `components` in your layer order statement, for example
59
+ `@layer c15t, components, app;`, so it does not land after your own layers.
60
+
18
61
  The standard stylesheet places rules in `@layer components`. For Tailwind 3,
19
62
  use the `styles.tw3.css` entry instead of the standard stylesheet, between the
20
63
  components and utilities directives in your Tailwind entry:
@@ -26,6 +69,18 @@ components and utilities directives in your Tailwind entry:
26
69
  @tailwind utilities;
27
70
  ```
28
71
 
72
+ Tailwind 3 also processes the dialog stylesheet the bundler loads, and it
73
+ rejects a stylesheet that uses `@layer components` without its own
74
+ `@tailwind components` directive. Add the c15t plugin before `tailwindcss` in
75
+ your PostCSS config. It removes the layer wrapper from c15t's stylesheets so
76
+ Tailwind 3 accepts them and its preflight does not override them:
77
+
78
+ ```js title="postcss.config.mjs"
79
+ export default {
80
+ plugins: ['@c15t/ui/postcss-tailwind3', 'tailwindcss', 'autoprefixer'],
81
+ };
82
+ ```
83
+
29
84
  Do not load both c15t stylesheet variants. For Tailwind 4 or unlayered CSS,
30
85
  inspect layer order before reaching for `!important`.
31
86
 
@@ -48,9 +103,17 @@ export const theme = defineTheme({
48
103
  });
49
104
  ```
50
105
 
51
- Install `@c15t/ui` if importing its theme helper directly. Pass `theme` in your
52
- provider options. `consentActions` selects styling by action role. A per-action
53
- entry overrides `primary`, which overrides `default`.
106
+ Install `@c15t/ui` if importing its theme helper directly. The browser does
107
+ not turn tokens into CSS. In React and Next.js, render
108
+ `<ConsentTheme theme={theme} />` where the app renders on the server, and pass
109
+ `theme` in your provider options for `consentActions`. Elsewhere, call
110
+ `generateThemeCSS(theme)` from `@c15t/ui/theme` on the server or at build time
111
+ and put the result in a `<style>` element or your stylesheet. See
112
+ [React styling](../frameworks/react/styling/overview.md) and
113
+ [Next.js styling](https://c15t.com/docs/frameworks/next/styling/overview).
114
+
115
+ `consentActions` selects styling by action role. A per-action entry overrides
116
+ `primary`, which overrides `default`.
54
117
 
55
118
  ## Target a prompt with CSS
56
119
 
@@ -97,14 +97,44 @@ it is `dialog`. These set that surface:
97
97
  * `useSetActiveUI()('dialog')` in your own component, or `openDialog()`
98
98
  from `useHeadlessConsentUI()` on the headless subpath.
99
99
 
100
- The component code is split into its own chunk. It downloads when the
101
- dialog first opens or when a visitor hovers or focuses a Customize button,
102
- and renders nothing until that chunk is ready. Passing `open={true}` or
103
- `showTrigger` loads it on mount.
100
+ The component code is split into its own chunk, so it is not part of the
101
+ first page load. It renders nothing until that chunk is ready, and the
102
+ chunk downloads at the first of these:
103
+
104
+ * The browser's first idle period after the page's `load` event, while the
105
+ banner is shown or a `ConsentDialogLink`, `ConsentDialogTrigger`,
106
+ `ConsentGate` button or other button that opens the dialog is mounted.
107
+ This covers a visitor who taps or presses Enter with no hover or focus
108
+ first. Browsers without `requestIdleCallback`, such as Safari, wait
109
+ 200 ms after `load` instead.
110
+ * Hover or focus on a button that opens the dialog.
111
+ * The dialog opening.
112
+
113
+ Passing `open={true}` or `showTrigger` loads it on mount. A visit with
114
+ saved consent and nothing on the page that opens the dialog never
115
+ downloads it.
116
+
117
+ Idle loading is skipped when the visitor has Save-Data on
118
+ (`navigator.connection.saveData`), on a `2g` or `slow-2g` connection, and
119
+ while offline; hover and focus still load it. To load the chunk only on
120
+ hover, focus or open, set `preloadDialog: 'intent'` in the provider
121
+ options. The default is `'idle'`. With Next.js, pass it through
122
+ `ConsentRoot`'s `options` prop.
123
+
124
+ If a preload fails, for example because the visitor is offline, nothing is
125
+ cached: the next hover, focus or open tries the download again.
126
+
127
+ The dialog's CSS travels in the same chunk and is applied before the dialog
128
+ renders, so your app's `styles.css` import stays small.
104
129
 
105
130
  Without an `open` prop, Escape closes the dialog. Save, Accept all and
106
- Reject all close it after the save succeeds; a save that fails keeps it
107
- open. With `open={true}`, these actions leave the dialog visible; the
131
+ Reject all close it as soon as the choice is recorded in the browser, in
132
+ the same task as the click; the exit animation still plays. The request to
133
+ the backend runs afterwards and never reopens the dialog: a failed request
134
+ keeps the choice, reports through `onError` and is retried later. See
135
+ [when a choice is saved](../../../guides/consent-state.md#when-a-choice-is-saved)
136
+ for the order of events. A draft that went stale because the policy changed keeps the dialog open
137
+ for review. With `open={true}`, these actions leave the dialog visible; the
108
138
  parent must set `open={false}` to close it. Clicking the backdrop does not
109
139
  close it. Closing discards toggles that were not saved: the next open starts
110
140
  from the recorded choice again. After a save from an uncontrolled dialog,
@@ -186,6 +216,15 @@ for the stock structure are `dialog.root`, `dialog.container`,
186
216
  `dialog.card`, `dialog.header`, `dialog.title`, `dialog.content`,
187
217
  `dialog.overlay`, `description.dialog`, `manager.footer` and `tag.dialog`.
188
218
 
219
+ `ConsentDialog` from the package root and from the
220
+ `c15t/react/consent-dialog` subpath is the deferred component described
221
+ under Behavior, and each `ConsentDialog.<Part>` loads with the same chunk.
222
+ `c15t/react/components/consent-dialog` exports the dialog without the
223
+ deferral, plus each part as a named export (`Card`, `Header`, `Overlay`,
224
+ `Root` and the rest). Importing from that path puts the dialog's code in
225
+ the bundle of every page that imports it, so the first open needs no
226
+ download but every visitor downloads the dialog.
227
+
189
228
  The compound parts import from `c15t/react` as properties of `ConsentDialog`,
190
229
  and `ConsentWidget` from the same module. See
191
230
  [Styling](../styling/overview.md) for slots and tokens.
@@ -84,9 +84,11 @@ scope cannot be granted. The built-in placeholder then shows `frame.policyBlocke
84
84
  the button, and a stored grant for that category stays blocked.
85
85
 
86
86
  When the provider starts from a snapshot prefetched on the server, the
87
- server renders the children or placeholder according to effective permission.
88
- A grant in the request cookie allows the embed only when policy and privacy
89
- signals permit it. Browser privacy signals detected during hydration can
87
+ server renders the placeholder for a denied category. For a granted
88
+ category it renders the empty wrapper, and the children mount once
89
+ hydration completes, so an iframe loads once even when the page streams
90
+ inside a `Suspense` boundary. A grant in the request cookie allows the
91
+ embed only when policy and privacy signals permit it. Browser privacy signals detected during hydration can
90
92
  withdraw that permission. In a browser-only setup the
91
93
  provider has no permission until it resolves policy on the client, so
92
94
  `ConsentGate` shows the placeholder first and swaps in the embed after resolution
@@ -142,7 +144,9 @@ center and is not restricted by policy or privacy signals such as GPC.
142
144
  Load the page with the category denied. The placeholder text names the
143
145
  category and the network panel shows no request to the embed's host. With a
144
146
  prefetched snapshot, the server HTML contains the placeholder for a denied
145
- category or the embed for a granted category. Browser-only initialization
147
+ category and neither placeholder nor embed for a granted category; the
148
+ embed appears after hydration and the network panel shows one request for
149
+ its document. Browser-only initialization
146
150
  shows the placeholder before policy resolves, then replaces it with the
147
151
  embed if the category is granted. After policy resolves, activate the
148
152
  placeholder button: the preference center opens. Turn the category on and
@@ -35,16 +35,18 @@ subscriptions. Keep it mounted across client navigation. Its initial mode and
35
35
  prefetch configuration belong to the runtime's creation, not a per-render
36
36
  mechanism for switching visitors or backends.
37
37
 
38
- | Option | Purpose |
39
- | --------------------- | --------------------------------------------------------------- |
40
- | `mode` | Hosted, offline or custom transport factory |
41
- | `scripts` | Consent-managed script configurations |
42
- | `callbacks` | Choice, permission and error events |
43
- | `theme`, `components` | Theme tokens and component slot attributes |
44
- | `presentation` | Prompt and preferences layout behavior |
45
- | `i18n` | Languages and message overrides |
46
- | `storageConfig` | Browser record storage configuration |
47
- | `enabled` | Set false only when intentionally bypassing consent enforcement |
38
+ | Option | Purpose |
39
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
40
+ | `mode` | Hosted, offline or custom transport factory |
41
+ | `scripts` | Consent-managed script configurations |
42
+ | `callbacks` | Choice, permission and error events |
43
+ | `theme`, `components` | Consent-action styles, theme slots and component slot attributes. Tokens render through `ConsentTheme` |
44
+ | `presentation` | Prompt and preferences layout behavior |
45
+ | `preloadDialog` | When the deferred `ConsentDialog` starts loading: `'idle'` (default) or `'intent'`; see [ConsentDialog](./consent-dialog.md#behavior) |
46
+ | `i18n` | Languages and message overrides |
47
+ | `storageConfig` | Browser record storage configuration |
48
+ | `enabled` | Set false only when intentionally bypassing consent enforcement |
49
+ | `reloadOnConsentRevoked` | Reload the page after the visitor turns off a granted category or vendor; defaults to `true` |
48
50
 
49
51
  `enabled: false` grants categories and allows gated loading while suppressing
50
52
  consent UI. It is not a way to fix failed initialization or hide a banner in
@@ -58,13 +60,31 @@ signals. Hydration and notice dismissal do not become explicit choices.
58
60
 
59
61
  An externally owned runtime can be passed through `runtime`. Its owner must
60
62
  start and dispose it. A provider borrowing that runtime must not initialize or
61
- dispose a second copy. Keep the runtime stable for the provider's lifetime and
62
- unmount the borrowing provider when disposing the runtime. IAB actions taken
63
- before its handle is ready wait for that handle. If the provider unmounts first,
64
- a pending `save()` rejects with an `AbortError` instead of waiting indefinitely.
63
+ dispose a second copy. Passing a different `runtime` moves the provider and its
64
+ hooks to the new runtime's kernel and unsubscribes them from the previous one.
65
+ The previous runtime keeps running until its owner disposes it. Switch the
66
+ provider to another runtime, or unmount it, before disposing the runtime it
67
+ renders. Changing between a borrowed runtime and a provider-created one still
68
+ requires remounting the provider. IAB actions taken before its handle is ready
69
+ wait for that handle. If the provider unmounts or switches runtimes first, a
70
+ pending `save()` rejects with an `AbortError` instead of waiting indefinitely.
65
71
  See [consent state](../../../guides/consent-state.md) and
66
72
  [deployment modes](../../../guides/deployment-modes.md).
67
73
 
74
+ ## Reload after revocation
75
+
76
+ Removing a script cannot stop code that already ran. A vendor's listeners,
77
+ timers and widgets keep working until the page unloads. When an accept, reject
78
+ or save turns off a category or vendor that was granted, the provider reloads
79
+ the page once the save request settles, so the next page runs only permitted
80
+ code. `onBeforeConsentRevocationReload` runs just before the reload. With a
81
+ [`consentSource`](../../../guides/shared-consent-controls.md), any withdrawal the
82
+ external CMP reports reloads the same way, including expiry.
83
+
84
+ Otherwise, expiry, policy changes and privacy signals do not reload the page. Set
85
+ `reloadOnConsentRevoked: false` only when every gated vendor stops itself on
86
+ revocation, for example through its own opt-out API.
87
+
68
88
  ## Clear data when consent is denied
69
89
 
70
90
  Set `options.clearOnRevocation` to declare cookies, localStorage keys, and
@@ -93,6 +93,9 @@ styled like a category row: the vendor's name and a switch on the header,
93
93
  and its description and privacy policy link behind the card's own expand
94
94
  control. Cards start collapsed, so a long list stays one line per vendor,
95
95
  and the category list scrolls inside the dialog rather than growing it.
96
+ A collapsed category or card renders its content element empty and mounts
97
+ the content the first time it opens, then keeps it mounted, so a long
98
+ vendor list costs nothing until a visitor expands its category.
96
99
  Vendor switches edit the same draft as the category
97
100
  switches and record on Save. Saving with one vendor off denies that vendor
98
101
  only; the category stays granted and its other vendors keep loading. While
@@ -191,6 +194,12 @@ Provider component slots for the stock structure are `manager.root`,
191
194
  `vendor-list.name`, `vendor-list.description`, `vendor-list.link`,
192
195
  `vendor-list.control` and `tag.manager`.
193
196
 
197
+ `ConsentWidget` from the package root and from the
198
+ `c15t/react/consent-widget` subpath loads its code in a separate chunk the
199
+ first time it renders, so a page that never renders it does not download
200
+ it. `c15t/react/components/consent-widget` exports the widget without that
201
+ deferral, plus each part as a named export.
202
+
194
203
  The compound parts import from `c15t/react` as properties of `ConsentWidget`.
195
204
  See [Styling](../styling/overview.md) for slots and tokens.
196
205
 
@@ -53,7 +53,14 @@ Analytics.
53
53
  Hidden optional categories do not need a choice to dismiss the banner. Their
54
54
  permissions still follow the backend policy, so hidden opt-in categories remain
55
55
  denied without a valid grant. React providers enable DOM iframe blocking and
56
- discovery by default; set `iframeBlocker: false` to disable it, or
56
+ discovery by default. The blocker loads when the first iframe with
57
+ `data-category` or `data-vendor` is on the page, so pages without one never
58
+ download it; until it runs, such an iframe that arrives with a `src` consent
59
+ does not allow is paused. Write gated iframes with `data-src`, not `src`. The
60
+ browser requests a `src` as soon as the iframe is in the document, before the
61
+ blocker can pause it, whether the iframe comes from the server HTML or a
62
+ client render. Set
63
+ `iframeBlocker: false` to disable it, or
57
64
  `iframeBlocker: { disableAutomaticBlocking: true }` to scan manually with
58
65
  `useIframeBlocker({ disableAutomaticBlocking: true }).processAllIframes()`.
59
66
 
@@ -51,8 +51,10 @@ This is an isolated action example, not a complete policy-aware banner. Use the
51
51
  stock UI or headless surface actions for a complete prompt. Do not call save on
52
52
  mount to persist a state derived from permissions.
53
53
 
54
- `useConsentManager` remains available for the combined UI API, including draft
55
- management. Prefer focused hooks for simple feature gates and use the headless
56
- UI hook when implementing a complete custom prompt. Read
57
- [consent state](../../../../guides/consent-state.md) before treating a callback as evidence
58
- of a choice.
54
+ Use focused hooks for feature gates. For a complete custom prompt or preference
55
+ center, use `useHeadlessConsentUI()` for surface actions and `useConsentDraft()`
56
+ for staged choices, inside one `ConsentDraftProvider` so the save commits the
57
+ same draft. v3 has no `useConsentManager()`; the
58
+ [upgrade guide](../../../upgrade-v3.md#replace-useconsentmanager) maps each of its
59
+ fields to a hook. Read [consent state](../../../guides/consent-state.md) before
60
+ treating a callback as evidence of a choice.
@@ -16,7 +16,9 @@ components inside an existing consent provider connected to Inth.
16
16
 
17
17
  Import the components from `c15t/react/iab` and render them inside your
18
18
  existing `ConsentProvider` from `c15t/react`. Import `c15t/react/iab/styles.css`
19
- at your global stylesheet entry.
19
+ at your global stylesheet entry, after `c15t/react/styles.css`: the IAB
20
+ stylesheet holds only the IAB rules and uses the tokens from the base
21
+ stylesheet.
20
22
 
21
23
  ```tsx title="src/iab-consent.tsx"
22
24
  import { IABProvider, IABConsentBanner, IABConsentDialog } from 'c15t/react/iab';
@@ -51,3 +53,121 @@ Test category choices alongside vendor, purpose, legitimate-interest and special
51
53
  feature controls used by your integration. Confirm the TC String and `__tcfapi`
52
54
  reflect the saved state, and verify the vendor's actual requests. A category-only
53
55
  banner test does not establish that the IAB flow works.
56
+
57
+ ## Restrict how vendors use a purpose
58
+
59
+ Publisher restrictions override a vendor's declared use of one purpose. Pass
60
+ them to `IABProvider` as `publisherRestrictions`. The vendor IDs below are
61
+ placeholders; use vendors from your list whose declarations allow each
62
+ restriction.
63
+
64
+ ```tsx title="src/iab-consent.tsx"
65
+ import { IABProvider, IABConsentBanner, IABConsentDialog } from 'c15t/react/iab';
66
+
67
+ const publisherRestrictions = [
68
+ // Vendors 10 and 755 may not use purpose 4.
69
+ { purposeId: 4, restrictionType: 0 as const, vendorIds: [10, 755] },
70
+ // Vendor 755 must use legitimate interest for purpose 7.
71
+ { purposeId: 7, restrictionType: 2 as const, vendorIds: [755] },
72
+ ];
73
+
74
+ export function IABConsent({ cmpId }: { cmpId: number }) {
75
+ return (
76
+ <IABProvider cmpId={cmpId} publisherRestrictions={publisherRestrictions}>
77
+ <IABConsentBanner />
78
+ <IABConsentDialog />
79
+ </IABProvider>
80
+ );
81
+ }
82
+ ```
83
+
84
+ Each restriction type has a requirement that depends on the vendor list:
85
+
86
+ | `restrictionType` | Meaning | Allowed when the vendor |
87
+ | ----------------- | -------------------------------------- | ------------------------------------------------------------------------------- |
88
+ | `0` | Purpose not allowed on any legal basis | declares the purpose for consent or legitimate interest |
89
+ | `1` | Consent required | declares the purpose for legitimate interest and lists it in `flexiblePurposes` |
90
+ | `2` | Legitimate interest required | declares the purpose for consent and lists it in `flexiblePurposes` |
91
+
92
+ Type `2` is never allowed for purposes 1, 3, 4, 5 and 6, which TCF allows
93
+ only with consent. Restrictions also require a service-specific CMP, the
94
+ default. Setting `isServiceSpecific: false` with restrictions is an error.
95
+ c15t copies the restrictions when the CMP starts, so changing your array
96
+ afterwards has no effect.
97
+
98
+ c15t checks restrictions when the vendor list loads and again before each TC
99
+ String is encoded. Unsupported restrictions include type `3`, which the spec
100
+ reserves, a vendor or purpose missing from the list, and a vendor with two
101
+ restriction types for one purpose. They are never dropped. Loading IAB
102
+ settings, saving and generating a TC String all reject with a
103
+ `PublisherRestrictionError` exported by `@c15t/iab`. On a `createIAB` handle
104
+ these are `whenReady()`, `save()` and `generateTCString()`. No TC String is
105
+ written and no consent is recorded. Retrying `whenReady()` does not fetch
106
+ another vendor list. The error lasts until a new vendor list is set up: with an
107
+ explicit `gvl` option that never happens, so saving keeps failing even if the
108
+ kernel later holds a different list. When the CMP follows the kernel's list
109
+ instead, a replacement list is checked again and saving works once it accepts
110
+ the restrictions. When a replacement vendor list makes a restriction
111
+ unsupported, c15t also clears the TC authority confirmed under the previous
112
+ list, so gated scripts stop until the configuration is fixed. Whenever c15t
113
+ withdraws the authority it confirmed, it also removes the TC String from the
114
+ `euconsent-v2` cookie and localStorage entry. Because the check reads the
115
+ current vendor list, a vendor that stops declaring a purpose or its flexibility
116
+ makes a restriction invalid. Narrow a `vendors` allowlist and your restrictions
117
+ together.
118
+
119
+ Saved TC Strings carry the restrictions in their `PubRestrictions` section, and
120
+ `__tcfapi('getTCData')` reports them as `publisher.restrictions`, keyed by
121
+ purpose ID, then vendor ID. A stored TC String whose restrictions differ from
122
+ the current configuration is not restored. If the visitor's stored choice is
123
+ otherwise current, the banner opens again, as it does after a policy change,
124
+ and closes once the visitor saves. IAB gates that depend on the TC String stay
125
+ denied until then. c15t also removes the superseded TC String from the
126
+ `euconsent-v2` cookie and localStorage entry, so vendors reading standard
127
+ storage do not pick it up. An unchanged configuration restores the stored TC
128
+ String without asking.
129
+ When reading a string written under TCF policy version 2 or 3, c15t accepts
130
+ type `2` for purposes 3 to 6, which those versions allowed. Strings c15t
131
+ writes always follow the current policy.
132
+
133
+ The React, Vue, Svelte and `@c15t/browser/iab` preference centres list each
134
+ vendor under the legal basis the restrictions leave it. A vendor whose purpose
135
+ now requires legitimate interest gets an objection control instead of a
136
+ consent toggle, and a prohibited purpose no longer lists the vendor. A
137
+ purpose whose vendors all use legitimate interest has no consent switch,
138
+ because turning one off would change nothing the vendors rely on; its
139
+ objection control is the opt-out. Such a purpose does not decide its c15t
140
+ category: the category follows the purposes in it that the visitor can
141
+ consent to. Legitimate interest never grants a category on its own. If none of
142
+ a category's purposes has a consent basis, the category needs all of them
143
+ consented, which only Accept all does. Stack switches
144
+ cover only the purposes some vendor processes on consent. Display-model rows
145
+ expose this as `hasConsentBasis`. Custom preference UIs can call
146
+ `applyPublisherRestrictionsToGVL` from `@c15t/iab/headless`, or pass
147
+ `publisherRestrictions` to `processGVLForDialog`, to get the same vendor
148
+ declarations. The configured restrictions are on the kernel's IAB state as
149
+ `publisherRestrictions`.
150
+
151
+ Consent-gated scripts, network rules and iframes with a `vendorId` also apply
152
+ the confirmed restrictions for that vendor:
153
+
154
+ * Type `0` blocks a target that declares the purpose in `iabPurposes` or
155
+ `iabLegIntPurposes`.
156
+ * Type `1` makes a purpose declared in `iabLegIntPurposes` require purpose and
157
+ vendor consent. Legitimate interest no longer satisfies it.
158
+ * Type `2` makes a purpose declared in `iabPurposes` require purpose and vendor
159
+ legitimate interest. Consent no longer satisfies it.
160
+
161
+ Types `1` and `2` block the target when the vendor list does not mark the
162
+ purpose as flexible for that vendor. Targets without a `vendorId` ignore
163
+ restrictions. A target that uses only legitimate interest once restrictions
164
+ apply is not blocked by a refused category, since it needs no consent; its
165
+ legitimate interest signals and the visitor's objection decide. GPC, opt-out
166
+ directives and strict scope still block it, and the category stays refused for
167
+ scripts that depend on it. Accept all grants the vendor signal a restriction
168
+ moves a purpose to, including legitimate interest for a vendor that declares
169
+ none. Legitimate interest a restriction introduces applies until the visitor
170
+ objects: the preference centres show it as allowed, and saving without touching
171
+ it encodes it as allowed. `introducedLegitimateInterest` from
172
+ `@c15t/iab/headless` lists those purposes and vendors for custom preference
173
+ UIs.
@@ -0,0 +1,146 @@
1
+ ---
2
+ title: Block network requests
3
+ description: Block fetch and XHR calls to third-party domains in a React app
4
+ until the visitor grants their consent category.
5
+ group: frameworks
6
+ ---
7
+
8
+ ## Add rules to the provider
9
+
10
+ The network blocker stops `fetch` and `XMLHttpRequest` calls to domains you
11
+ list until the visitor grants their category. Use it for beacons and API
12
+ calls that bypass script loading, such as a pixel fired by an SDK that is
13
+ already on the page.
14
+
15
+ Add `networkBlocker` to the `ConsentProvider` options from your
16
+ [quickstart](./quickstart.md). Set the URL to the endpoint
17
+ supplied by your Inth project.
18
+
19
+ ```tsx title="components/consent.tsx"
20
+ 'use client';
21
+
22
+ import type { ReactNode } from 'react';
23
+ import { ConsentBanner, ConsentDialog, ConsentProvider, hosted } from 'c15t/react';
24
+
25
+ const mode = hosted({ url: 'https://your-project.inth.app' });
26
+ const networkBlocker = {
27
+ rules: [
28
+ { id: 'google-analytics', domain: 'google-analytics.com', category: 'measurement' },
29
+ { id: 'meta-pixel', domain: 'facebook.com', pathIncludes: '/tr', category: 'marketing' },
30
+ ],
31
+ };
32
+
33
+ export function Consent({ children }: { children: ReactNode }) {
34
+ return (
35
+ <ConsentProvider options={{ mode, networkBlocker }}>
36
+ {children}
37
+ <ConsentBanner />
38
+ <ConsentDialog />
39
+ </ConsentProvider>
40
+ );
41
+ }
42
+ ```
43
+
44
+ Render your app inside `ConsentProvider`. Blocking starts when it renders, so
45
+ components outside it that send requests while rendering, or modules evaluated
46
+ before it, are not covered. See the limits below.
47
+
48
+ ## Match requests with rules
49
+
50
+ Each rule names a `domain` and the consent `category` a request needs. The
51
+ domain also matches its subdomains: `google-analytics.com` covers
52
+ `www.google-analytics.com`. `pathIncludes` narrows the rule to paths that
53
+ contain a substring, and `methods` narrows it to HTTP methods. A request is
54
+ blocked when a matching rule's condition is not met by the visitor's
55
+ effective permissions.
56
+
57
+ `category` takes the same conditions as scripts:
58
+
59
+ ```ts
60
+ { category: 'measurement' }
61
+ { category: { and: ['measurement', 'marketing'] } }
62
+ { category: { or: ['measurement', 'marketing'] } }
63
+ ```
64
+
65
+ Add `vendor` to also block the request while the visitor has turned that
66
+ vendor off. IAB TCF rules use `vendorId` and the `iab*` purpose fields
67
+ instead.
68
+
69
+ | Option | Default | Purpose |
70
+ | -------------------- | -------- | ------------------------------------------------------------ |
71
+ | `rules` | required | Rules described above |
72
+ | `enabled` | `true` | Set `false` to keep the rules but stop blocking |
73
+ | `logBlockedRequests` | `true` | Log each blocked request with `console.warn` |
74
+ | `onRequestBlocked` | none | Called with `{ method, url, rule }` for each blocked request |
75
+
76
+ ## What a blocked request looks like
77
+
78
+ The blocker wraps `window.fetch` and `XMLHttpRequest`. A blocked `fetch`
79
+ resolves to a `451` response with the status text
80
+ `Request blocked by consent`, and nothing is sent. A blocked XHR is aborted
81
+ and fires an `error` event. Requests that match no rule are not delayed.
82
+
83
+ ## When blocking starts
84
+
85
+ The provider holds matching requests from its first render in the browser,
86
+ before any of its children render or run effects. That covers requests from
87
+ child components, including their mount effects, and from effects in
88
+ components rendered next to the provider. The blocker module itself loads
89
+ after mount and decides each held request. Apps without `networkBlocker` do
90
+ not download it.
91
+
92
+ The standalone `useNetworkBlocker` hook works the same way from the first
93
+ render of the component that calls it. That render patches `fetch` and
94
+ `XMLHttpRequest`. If React throws the render away and never commits it, the
95
+ hold ends after 10 seconds. Nothing checked consent for the requests it held,
96
+ so they fail the way the blocker fails a blocked request: a 451 response for
97
+ `fetch`, a failed XHR. The same happens when the component unmounts before
98
+ the blocker loads.
99
+
100
+ While consent is unknown, a matching request that would be blocked waits
101
+ instead of failing. Consent is unknown until the policy has loaded, which is
102
+ also when a returning visitor's stored choice takes effect. The request is
103
+ then sent if the choice allows it and blocked otherwise. If the policy fails
104
+ to load, optional categories stay denied and waiting requests are blocked.
105
+ If the policy request never finishes, they keep waiting and are never sent.
106
+
107
+ A synchronous XHR cannot wait. Before the blocker module has loaded, a
108
+ matching one throws a `NetworkError` from `send()`. After that, one that
109
+ consent does not allow yet is blocked.
110
+
111
+ ## What the network blocker cannot stop
112
+
113
+ The blocker only sees requests made after the provider starts rendering in
114
+ the browser, through `fetch` or `XMLHttpRequest`. It cannot stop:
115
+
116
+ * Code that runs before the provider renders: inline scripts in the HTML,
117
+ third-party tags in `<head>`, scripts loaded before hydration (such as
118
+ `next/script` with `beforeInteractive`), and client modules that evaluate
119
+ earlier. Webpack builds evaluate a route's client component modules when
120
+ its chunk loads, so their top-level code runs first. Turbopack evaluates a
121
+ client component module when its first element renders, which inside the
122
+ provider is after blocking starts.
123
+ * Code that saved its own reference to `fetch` or `XMLHttpRequest` before the
124
+ provider rendered.
125
+ * `navigator.sendBeacon`, `WebSocket`, `EventSource`, and requests made by
126
+ `<img>`, `<script>` or `<iframe>` elements, web workers and service
127
+ workers.
128
+ * With the standalone `useNetworkBlocker` hook instead of the provider
129
+ option, requests sent before the component that calls it renders.
130
+ Blocking starts in that component's first render, not the provider's.
131
+
132
+ Keep tracking calls out of that window:
133
+
134
+ * Send them from an effect or an event handler, never at module top level.
135
+ * Load vendor SDKs through `scripts` instead of a `<script>` tag or
136
+ `next/script`, so they wait for consent before they run at all.
137
+ * Check `useConsent('measurement')` (or the category you need) before you
138
+ call a vendor from your own code, and treat the blocker as a backstop.
139
+
140
+ ## Verify
141
+
142
+ Open the browser's network panel, clear site data and reload. Requests
143
+ matching a rule should not appear until you grant their category; with
144
+ logging on, the console lists each blocked request. Reject, reload, and
145
+ check that they still do not appear. The
146
+ [verification guide](../../guides/verify-consent.md) covers automated checks.
@@ -61,3 +61,9 @@ already wrote. Add `clearOnRevocation` to `ConsentProvider.options` to remove
61
61
  declared data when its category is denied. See
62
62
  [clear on revocation](../../integrations/clear-on-revocation.md) for configuration
63
63
  and browser limits.
64
+
65
+ ## Shared lifecycle controls
66
+
67
+ See [shared consent controls](../../guides/shared-consent-controls.md) for external CMPs,
68
+ preference delegation, withdrawal reloads, and application events. These controls
69
+ use the same core runtime across frameworks.