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

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 (248) hide show
  1. package/AGENTS.md +91 -67
  2. package/README.md +21 -17
  3. package/SKILL.md +34 -0
  4. package/dist/aggregate-components.js +1 -1
  5. package/dist/chunk-warming.js +1 -1
  6. package/dist/component-hooks/use-headless-consent-ui.js +1 -1
  7. package/dist/component-hooks/use-headless-iab-consent-ui.js +1 -1
  8. package/dist/component-hooks/use-panel-trigger.js +1 -1
  9. package/dist/components/consent-gate/atoms.js +1 -1
  10. package/dist/components/consent-gate/consent-gate.js +1 -1
  11. package/dist/components/consent-preferences-link/consent-preferences-link.js +1 -1
  12. package/dist/components/iab-panel/atoms/card.js +1 -1
  13. package/dist/components/iab-panel/atoms/feature-item.js +2 -0
  14. package/dist/components/iab-panel/atoms/header.js +1 -1
  15. package/dist/components/iab-panel/atoms/purpose-item.js +1 -1
  16. package/dist/components/iab-panel/atoms/stack-item.js +1 -1
  17. package/dist/components/iab-panel/iab-panel.js +1 -1
  18. package/dist/components/iab-panel/index.js +1 -1
  19. package/dist/components/iab-prompt/atoms/overlay.js +1 -1
  20. package/dist/components/iab-prompt/atoms/root.js +1 -1
  21. package/dist/components/panel/atoms/card.js +1 -1
  22. package/dist/components/panel/atoms/overlay.js +1 -1
  23. package/dist/components/panel/atoms/root.js +1 -1
  24. package/dist/components/panel/index.js +1 -1
  25. package/dist/components/panel/panel.js +1 -1
  26. package/dist/components/panel-trigger/atoms/button.js +1 -1
  27. package/dist/components/panel-trigger/atoms/icon.js +1 -1
  28. package/dist/components/panel-trigger/atoms/root.js +1 -1
  29. package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
  30. package/dist/components/panel-trigger/atoms/trigger-icons.js +1 -0
  31. package/dist/components/panel-trigger/panel-trigger-toolbar.js +1 -1
  32. package/dist/components/preferences/atoms/accordion.js +1 -1
  33. package/dist/components/preferences/atoms/root.js +1 -1
  34. package/dist/components/preferences/atoms/vendor-list.js +1 -1
  35. package/dist/components/preferences/index.js +1 -1
  36. package/dist/components/prompt/atoms/overlay.js +1 -1
  37. package/dist/components/prompt/atoms/root.js +1 -1
  38. package/dist/components/prompt/components.js +1 -1
  39. package/dist/components/prompt/policy-actions.js +1 -1
  40. package/dist/components/shared/libs/compose-refs.js +1 -0
  41. package/dist/components/shared/libs/slot.js +1 -1
  42. package/dist/components/shared/primitives/button.js +1 -1
  43. package/dist/components/shared/ui/accordion/accordion.js +1 -1
  44. package/dist/components/shared/ui/collapsible/collapsible.js +1 -1
  45. package/dist/components/shared/ui/dialog/dialog.js +1 -1
  46. package/dist/components/shared/ui/icon.js +1 -1
  47. package/dist/components/shared/ui/logo.js +1 -1
  48. package/dist/components/shared/ui/preference-item/preference-item.js +1 -1
  49. package/dist/components/shared/ui/switch/switch.js +1 -1
  50. package/dist/components/shared/ui/tabs/tabs.js +1 -1
  51. package/dist/consent-theme.js +1 -0
  52. package/dist/context/consent-tracking-context.js +1 -1
  53. package/dist/devtools.js +1 -1
  54. package/dist/draft.js +1 -1
  55. package/dist/external-iab-context.js +1 -1
  56. package/dist/headless.js +1 -1
  57. package/dist/hooks/use-committed-ref.js +2 -0
  58. package/dist/hooks/use-focus-trap.js +1 -1
  59. package/dist/hooks.js +1 -1
  60. package/dist/iab/styles.tw3.css +5 -45
  61. package/dist/iab-context.js +1 -1
  62. package/dist/index.js +1 -1
  63. package/dist/kernel-selector.js +1 -0
  64. package/dist/module-hooks/iframe-blocker.js +1 -1
  65. package/dist/module-hooks/network-blocker.js +1 -1
  66. package/dist/module-hooks/network-hold.js +2 -0
  67. package/dist/module-hooks/persistence.js +1 -1
  68. package/dist/panel.js +1 -1
  69. package/dist/postcss-tailwind3.js +1 -0
  70. package/dist/preferences.js +1 -2
  71. package/dist/provider.js +1 -1
  72. package/dist/server/fetch-ssr-data.js +1 -1
  73. package/dist/server/headers.js +1 -1
  74. package/dist/server/normalize-url.js +1 -1
  75. package/dist/styles.tw3.css +16 -61
  76. package/dist/transports/offline.js +1 -1
  77. package/dist/ui-save.js +1 -1
  78. package/dist/utils/deferred-module.js +1 -1
  79. package/dist/utils/merge-slot-props.js +1 -1
  80. package/dist/utils/theme-utils.js +1 -1
  81. package/dist/version.js +1 -1
  82. package/dist-types/chunk-warming.d.ts +41 -8
  83. package/dist-types/component-hooks/use-headless-consent-ui.d.ts +1 -1
  84. package/dist-types/component-hooks/use-headless-iab-consent-ui.d.ts +1 -1
  85. package/dist-types/components/iab-panel/atoms/feature-item.d.ts +28 -0
  86. package/dist-types/components/iab-panel/atoms/purpose-item.d.ts +4 -1
  87. package/dist-types/components/iab-panel/atoms/stack-item.d.ts +6 -1
  88. package/dist-types/components/iab-panel/iab-panel.d.ts +2 -2
  89. package/dist-types/components/iab-panel/index.d.ts +6 -2
  90. package/dist-types/components/iab-panel/use-iab-translations.d.ts +4 -0
  91. package/dist-types/components/iab-prompt/iab-prompt.d.ts +2 -2
  92. package/dist-types/components/iab-prompt/index.d.ts +2 -2
  93. package/dist-types/components/panel/atoms/card.d.ts +1 -0
  94. package/dist-types/components/panel/index.d.ts +1 -0
  95. package/dist-types/components/panel/panel.d.ts +1 -1
  96. package/dist-types/components/panel-trigger/atoms/trigger-icons.d.ts +20 -0
  97. package/dist-types/components/preferences/index.d.ts +1 -0
  98. package/dist-types/components/prompt/components.d.ts +6 -0
  99. package/dist-types/components/shared/libs/compose-refs.d.ts +20 -0
  100. package/dist-types/components/shared/ui/accordion/accordion.d.ts +1 -0
  101. package/dist-types/components/shared/ui/collapsible/collapsible.d.ts +1 -0
  102. package/dist-types/components/shared/ui/icon.d.ts +2 -1
  103. package/dist-types/components/shared/ui/logo.d.ts +0 -10
  104. package/dist-types/components/shared/ui/preference-item/preference-item.d.ts +14 -0
  105. package/dist-types/components/shared/ui/switch/switch.d.ts +1 -0
  106. package/dist-types/components/shared/ui/tabs/tabs.d.ts +1 -0
  107. package/dist-types/consent-theme.d.ts +38 -0
  108. package/dist-types/context/consent-tracking-context.d.ts +10 -0
  109. package/dist-types/context/theme-context.d.ts +2 -2
  110. package/dist-types/context.d.ts +6 -0
  111. package/dist-types/devtools.d.ts +13 -2
  112. package/dist-types/draft.d.ts +50 -7
  113. package/dist-types/headless.d.ts +0 -1
  114. package/dist-types/hooks/use-committed-ref.d.ts +24 -0
  115. package/dist-types/hooks/use-focus-trap.d.ts +3 -1
  116. package/dist-types/hooks.d.ts +56 -13
  117. package/dist-types/iab.d.ts +1 -1
  118. package/dist-types/index.d.ts +4 -3
  119. package/dist-types/kernel-selector.d.ts +66 -0
  120. package/dist-types/module-hooks/iframe-blocker.d.ts +27 -0
  121. package/dist-types/module-hooks/network-blocker.d.ts +23 -0
  122. package/dist-types/module-hooks/network-hold.d.ts +47 -0
  123. package/dist-types/panel.d.ts +13 -1
  124. package/dist-types/postcss-tailwind3.d.ts +21 -0
  125. package/dist-types/preferences.d.ts +12 -1
  126. package/dist-types/provider.d.ts +62 -12
  127. package/dist-types/server/headers.d.ts +14 -4
  128. package/dist-types/server/normalize-url.d.ts +14 -7
  129. package/dist-types/server/types.d.ts +12 -1
  130. package/dist-types/transports/offline.d.ts +7 -16
  131. package/dist-types/ui-config-context.d.ts +9 -1
  132. package/dist-types/ui-save.d.ts +21 -1
  133. package/dist-types/utils/deferred-module.d.ts +3 -1
  134. package/dist-types/utils/merge-slot-props.d.ts +15 -1
  135. package/dist-types/utils/theme-utils.d.ts +17 -2
  136. package/dist-types/version.d.ts +1 -1
  137. package/docs/README.md +91 -67
  138. package/docs/assets/v3/bottom-bar.png +0 -0
  139. package/docs/assets/v3/brand-card.png +0 -0
  140. package/docs/assets/v3/brand-preferences.png +0 -0
  141. package/docs/assets/v3/choice-wall.png +0 -0
  142. package/docs/assets/v3/headless-bar-html.png +0 -0
  143. package/docs/assets/v3/headless-bar-mobile.png +0 -0
  144. package/docs/assets/v3/headless-bar.png +0 -0
  145. package/docs/assets/v3/slim-bar.png +0 -0
  146. package/docs/concepts/choose-your-setup.md +87 -0
  147. package/docs/{frameworks/react/concepts → concepts}/consent-categories.md +30 -8
  148. package/docs/concepts/consent-state.md +357 -0
  149. package/docs/{guides → concepts}/data-fetching.md +31 -27
  150. package/docs/concepts/how-consent-works.md +123 -0
  151. package/docs/{frameworks/react/concepts/policy-presets.md → concepts/policies.md} +7 -59
  152. package/docs/customization/class-names.md +202 -0
  153. package/docs/customization/dark-mode.md +157 -0
  154. package/docs/customization/motion.md +119 -0
  155. package/docs/customization/overview.md +67 -33
  156. package/docs/customization/recipes.md +839 -47
  157. package/docs/customization/slots.md +216 -35
  158. package/docs/customization/stylesheets.md +147 -0
  159. package/docs/customization/tailwind.md +842 -0
  160. package/docs/customization/tokens.md +166 -36
  161. package/docs/customization/translations.md +61 -3
  162. package/docs/frameworks/react/callbacks.md +118 -0
  163. package/docs/frameworks/react/clear-on-revocation.md +106 -0
  164. package/docs/frameworks/react/components/consent-banner.md +42 -28
  165. package/docs/frameworks/react/components/consent-dialog-link.md +13 -11
  166. package/docs/frameworks/react/components/consent-dialog-trigger.md +81 -76
  167. package/docs/frameworks/react/components/consent-dialog.md +100 -55
  168. package/docs/frameworks/react/components/consent-gate.md +45 -24
  169. package/docs/frameworks/react/components/consent-provider.md +126 -0
  170. package/docs/frameworks/react/components/consent-widget.md +32 -20
  171. package/docs/frameworks/react/components/dev-tools.md +52 -84
  172. package/docs/frameworks/react/components.md +105 -0
  173. package/docs/frameworks/react/compose.md +354 -0
  174. package/docs/frameworks/react/content-security-policy.md +126 -0
  175. package/docs/frameworks/react/customize.md +250 -0
  176. package/docs/frameworks/react/embeds.md +89 -0
  177. package/docs/frameworks/react/hooks.md +65 -0
  178. package/docs/frameworks/react/iab.md +222 -0
  179. package/docs/frameworks/react/network-blocker.md +140 -0
  180. package/docs/frameworks/react/quickstart.md +139 -101
  181. package/docs/frameworks/react/rendering.md +128 -0
  182. package/docs/frameworks/react/scripts.md +115 -0
  183. package/docs/frameworks/react/translations.md +155 -0
  184. package/docs/frameworks/react/troubleshooting.md +92 -47
  185. package/docs/frameworks/react/vendor-consent.md +170 -0
  186. package/docs/guides/banner-experiments.md +654 -0
  187. package/docs/guides/troubleshooting.md +120 -47
  188. package/docs/guides/verify-consent.md +81 -49
  189. package/docs/integrations/adobe-analytics.md +168 -160
  190. package/docs/integrations/ahrefs-analytics.md +153 -155
  191. package/docs/integrations/amplitude.md +163 -157
  192. package/docs/integrations/building-integrations.md +136 -37
  193. package/docs/integrations/clearbit.md +155 -155
  194. package/docs/integrations/cloudflare-web-analytics.md +157 -157
  195. package/docs/integrations/cloudflare-zaraz.md +210 -262
  196. package/docs/integrations/crisp.md +165 -159
  197. package/docs/integrations/databuddy.md +158 -174
  198. package/docs/integrations/fathom-analytics.md +159 -157
  199. package/docs/integrations/front-chat.md +322 -0
  200. package/docs/integrations/google-maps.md +119 -84
  201. package/docs/integrations/google-tag-manager.md +179 -164
  202. package/docs/integrations/google-tag.md +164 -161
  203. package/docs/integrations/heap.md +164 -156
  204. package/docs/integrations/hightouch.md +162 -158
  205. package/docs/integrations/hotjar.md +160 -156
  206. package/docs/integrations/intercom.md +184 -154
  207. package/docs/integrations/klaviyo.md +486 -0
  208. package/docs/integrations/linkedin-insights.md +175 -151
  209. package/docs/integrations/logrocket.md +161 -157
  210. package/docs/integrations/matomo-analytics.md +189 -179
  211. package/docs/integrations/meta-pixel.md +189 -151
  212. package/docs/integrations/microsoft-clarity.md +164 -156
  213. package/docs/integrations/microsoft-uet.md +149 -155
  214. package/docs/integrations/mixpanel-analytics.md +156 -161
  215. package/docs/integrations/one-dollar-stats.md +306 -0
  216. package/docs/integrations/openai-pixel.md +205 -302
  217. package/docs/integrations/overview.md +143 -83
  218. package/docs/integrations/pinterest-tag.md +329 -0
  219. package/docs/integrations/pirsch.md +170 -160
  220. package/docs/integrations/plausible-analytics.md +173 -159
  221. package/docs/integrations/posthog.md +227 -242
  222. package/docs/integrations/promptwatch.md +155 -155
  223. package/docs/integrations/reddit-pixel.md +186 -158
  224. package/docs/integrations/rudderstack.md +202 -187
  225. package/docs/integrations/rybbit-analytics.md +172 -161
  226. package/docs/integrations/segment.md +183 -155
  227. package/docs/integrations/snapchat-pixel.md +187 -157
  228. package/docs/integrations/tiktok-pixel.md +172 -151
  229. package/docs/integrations/umami-analytics.md +164 -159
  230. package/docs/integrations/vercel-analytics.md +167 -158
  231. package/docs/integrations/x-pixel.md +177 -151
  232. package/docs/integrations/youtube.md +122 -87
  233. package/docs/upgrade-v3.md +490 -354
  234. package/package.json +15 -9
  235. package/dist/component-hooks/use-manager.js +0 -2
  236. package/dist-types/component-hooks/use-manager.d.ts +0 -45
  237. package/docs/assets/v3/brand-bar.png +0 -0
  238. package/docs/assets/v3/mobile-card.png +0 -0
  239. package/docs/assets/v3/preferences.png +0 -0
  240. package/docs/frameworks/react/components/consent-manager-provider.md +0 -75
  241. package/docs/frameworks/react/hooks/use-consent-manager/overview.md +0 -58
  242. package/docs/frameworks/react/iab/overview.md +0 -53
  243. package/docs/frameworks/react/script-loader.md +0 -63
  244. package/docs/frameworks/react/styling/overview.md +0 -217
  245. package/docs/guides/consent-state.md +0 -60
  246. package/docs/guides/deployment-modes.md +0 -75
  247. package/docs/integrations/clear-on-revocation.md +0 -167
  248. package/docs/integrations/granular-consent.md +0 -208
@@ -16,15 +16,17 @@ same `ConsentProvider`; the [quickstart](../quickstart.md) mounts both.
16
16
  import { ConsentDialogLink } from 'c15t/react';
17
17
 
18
18
  export function SiteFooter() {
19
- return (
20
- <footer>
21
- <nav aria-label="Legal">
22
- <a href="/privacy">Privacy policy</a>
23
- <a href="/terms">Terms</a>
24
- <ConsentDialogLink className="footer-link">Privacy settings</ConsentDialogLink>
25
- </nav>
26
- </footer>
27
- );
19
+ return (
20
+ <footer>
21
+ <nav aria-label="Legal">
22
+ <a href="/privacy">Privacy policy</a>
23
+ <a href="/terms">Terms</a>
24
+ <ConsentDialogLink className="footer-link">
25
+ Privacy settings
26
+ </ConsentDialogLink>
27
+ </nav>
28
+ </footer>
29
+ );
28
30
  }
29
31
  ```
30
32
 
@@ -37,8 +39,8 @@ To reuse an existing link component, pass `asChild`:
37
39
 
38
40
  ```tsx
39
41
  <ConsentDialogLink asChild>
40
- <a href="#cookie-preferences">Manage cookies</a>
41
- </ConsentDialogLink>
42
+ <a href="#cookie-preferences">Manage cookies</a>
43
+ </ConsentDialogLink>;
42
44
  ```
43
45
 
44
46
  ## Props
@@ -1,35 +1,20 @@
1
1
  ---
2
2
  title: ConsentDialogTrigger
3
- description: Floating button and toolbar that reopen the preference center after
4
- the first prompt.
3
+ description: Add the c15t ConsentDialogTrigger floating button or toolbar inside
4
+ a React ConsentProvider so visitors can reopen the preference center after the
5
+ first prompt.
5
6
  group: frameworks
6
7
  ---
7
8
 
8
- ## Usage
9
+ ## Add the trigger inside ConsentProvider
9
10
 
10
- `ConsentDialogTrigger` is a draggable floating button that opens the preference center. Place it once, next to the banner and dialog, so visitors can revisit their choices without a footer link.
11
+ `ConsentDialogTrigger` is a draggable floating button that opens the preference center. Place it once inside `ConsentProvider`, next to the banner and dialog from the [quickstart](../quickstart.md), so visitors can revisit their choices without a footer link.
11
12
 
12
- ```tsx
13
- import {
14
- ConsentBanner,
15
- ConsentDialog,
16
- ConsentDialogTrigger,
17
- ConsentProvider,
18
- offline,
19
- } from 'c15t/react';
20
- import { policyRulePresets } from '@c15t/schema';
21
-
22
- const mode = offline({ policyRules: [policyRulePresets.europeOptIn()] });
23
-
24
- export function App() {
25
- return (
26
- <ConsentProvider options={{ mode }}>
27
- <ConsentBanner />
28
- <ConsentDialog />
29
- <ConsentDialogTrigger showWhen="after-prompt" />
30
- </ConsentProvider>
31
- );
32
- }
13
+ ```tsx title="src/consent.tsx"
14
+ import { ConsentDialogTrigger } from 'c15t/react';
15
+
16
+ // Inside the ConsentProvider from your quickstart, next to the dialog:
17
+ <ConsentDialogTrigger showWhen="after-prompt" />;
33
18
  ```
34
19
 
35
20
  `showWhen="after-prompt"` keeps the button out of the way while a choice or notice is still owed and shows it once the visitor has answered, so the banner and the trigger never compete for the same corner. The default, `always`, shows it whenever the preference center is closed.
@@ -56,43 +41,63 @@ Use `ConsentDialogTriggerToolbar` when you want app-owned controls beside the pr
56
41
  import { ConsentDialogTriggerToolbar } from 'c15t/react';
57
42
 
58
43
  <ConsentDialogTriggerToolbar
59
- ariaLabel="Site controls"
60
- actions={[
61
- {
62
- id: 'theme',
63
- label: isDark ? 'Switch to light theme' : 'Switch to dark theme',
64
- icon: isDark ? <SunIcon /> : <MoonIcon />,
65
- pressed: isDark,
66
- onSelect: toggleColorScheme,
67
- },
68
- {
69
- id: 'support',
70
- label: 'Open support chat',
71
- icon: <ChatIcon />,
72
- onSelect: openSupportChat,
73
- },
74
- ]}
75
- preferences={{
76
- icon: 'fingerprint',
77
- label: 'Manage privacy settings',
78
- }}
79
- />
44
+ ariaLabel="Site controls"
45
+ actions={[
46
+ {
47
+ id: 'theme',
48
+ label: isDark ? 'Switch to light theme' : 'Switch to dark theme',
49
+ icon: isDark ? <SunIcon /> : <MoonIcon />,
50
+ pressed: isDark,
51
+ onSelect: toggleColorScheme,
52
+ },
53
+ {
54
+ id: 'support',
55
+ label: 'Open support chat',
56
+ icon: <ChatIcon />,
57
+ onSelect: openSupportChat,
58
+ },
59
+ ]}
60
+ preferences={{ icon: 'fingerprint', label: 'Manage privacy settings' }}
61
+ />;
80
62
  ```
81
63
 
82
64
  Each custom action needs a stable `id`, an accessible `label`, an `icon`, and an `onSelect` callback. Use `pressed` for toggle actions and `disabled` for unavailable ones. Your app owns the state behind each action. The preferences action moves to the edge nearest the toolbar's snapped corner while custom actions keep their configured order.
83
65
 
84
- The toolbar does not depend on the consent policy. Your actions always render, so a theme toggle stays available while a banner is up. On the toolbar, `showWhen` applies only to the built-in preferences action: `after-prompt` hides that one item while a choice or notice is owed and shows it once the visitor has answered, and `never` leaves it out. The toolbar renders nothing only when it has no visible item. On the single `ConsentDialogTrigger`, `showWhen` applies to the whole control.
85
-
86
- No policy, no UI. When no rule has resolved, because resolution failed, no rule matched and you set no default, or init is still withheld, the built-in preferences action and the single `ConsentDialogTrigger` render nothing; app-owned toolbar actions still do. They appear as soon as a rule resolves, without a remount. A rule with `model: 'none'` owes no rights, so the built-in action and the single trigger hide under it too, unless the rule adds `rights: ['preferences']`. The policy chooses what the control says, you choose where and when it sits, and it disappears only when there is nothing to manage.
87
-
88
- The built-in action names the strongest right the active rule guarantees. Under a rule that carries the `opt-out` right, such as a US opt-out notice, its accessible name is the translated "Do not sell or share my data" and the button carries `data-right="opt-out"`. Under any other rule it reads "Manage preferences" with `data-right="preferences"`. Either way it opens the preference center, and a `preferences.label` you pass replaces the default. The button also carries `data-c15t-rights` with every right on the rule.
89
-
90
66
  Toolbars are horizontal by default. Set `orientation="vertical"` to stack the actions and switch arrow-key navigation to up and down:
91
67
 
92
68
  ```tsx
93
- <ConsentDialogTriggerToolbar orientation="vertical" actions={toolbarActions} />
69
+ <ConsentDialogTriggerToolbar orientation="vertical" actions={toolbarActions} />;
94
70
  ```
95
71
 
72
+ ### When the controls show
73
+
74
+ Your toolbar actions do not depend on the consent policy, so a theme toggle
75
+ stays available while a banner is up. The built-in preferences action, and the
76
+ single `ConsentDialogTrigger`, follow the policy:
77
+
78
+ | Situation | Built-in action and `ConsentDialogTrigger` |
79
+ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
80
+ | A choice or notice is still owed, with `showWhen="after-prompt"` | Hidden until the visitor answers |
81
+ | `showWhen="never"` on the toolbar | Left out of the toolbar |
82
+ | No policy rule has resolved: resolution failed, no rule matched and you set no default, or init is still withheld | Hidden; appears without a remount once a rule resolves |
83
+ | The rule has `model: 'none'` | Hidden, unless the rule adds `rights: ['preferences']` |
84
+ | Any other case | Shown |
85
+
86
+ On the toolbar, `showWhen` applies only to the built-in action. On the single
87
+ `ConsentDialogTrigger`, it applies to the whole control. The toolbar renders
88
+ nothing only when it has no visible item.
89
+
90
+ ### What the built-in action says
91
+
92
+ * Under a rule with the `opt-out` right, such as a US opt-out notice, its
93
+ accessible name is the translated "Do not sell or share my data" and it
94
+ carries `data-right="opt-out"`.
95
+ * Under any other rule it reads "Manage preferences" and carries
96
+ `data-right="preferences"`.
97
+ * Either way it opens the preference center. A `preferences.label` you pass
98
+ replaces the default name, and the button carries `data-c15t-rights` with
99
+ every right on the rule.
100
+
96
101
  ### Toolbar props
97
102
 
98
103
  | Prop | Type | Default | Description |
@@ -113,30 +118,30 @@ The toolbar follows the standard precedence: bundled styles, then provider slots
113
118
 
114
119
  ```tsx
115
120
  <ConsentProvider
116
- options={{
117
- mode: offline({ policyRules: [policyRulePresets.europeOptIn()] }),
118
- components: {
119
- trigger: {
120
- toolbar: { className: 'my-toolbar' },
121
- toolbarItem: { className: 'my-toolbar-item' },
122
- toolbarIcon: { className: 'my-toolbar-icon' },
123
- },
124
- },
125
- }}
121
+ options={{
122
+ mode,
123
+ components: {
124
+ trigger: {
125
+ toolbar: { className: 'my-toolbar' },
126
+ toolbarItem: { className: 'my-toolbar-item' },
127
+ toolbarIcon: { className: 'my-toolbar-icon' },
128
+ },
129
+ },
130
+ }}
126
131
  >
127
- <ConsentDialogTriggerToolbar
128
- className="fixed-toolbar"
129
- actions={[
130
- {
131
- id: 'support',
132
- label: 'Open support chat',
133
- icon: <ChatIcon />,
134
- onSelect: openSupportChat,
135
- className: 'support-action',
136
- },
137
- ]}
138
- />
139
- </ConsentProvider>
132
+ <ConsentDialogTriggerToolbar
133
+ className="fixed-toolbar"
134
+ actions={[
135
+ {
136
+ id: 'support',
137
+ label: 'Open support chat',
138
+ icon: <ChatIcon />,
139
+ onSelect: openSupportChat,
140
+ className: 'support-action',
141
+ },
142
+ ]}
143
+ />
144
+ </ConsentProvider>;
140
145
  ```
141
146
 
142
147
  The toolbar slot keys are `trigger.toolbar`, `trigger.toolbarItem`, and `trigger.toolbarIcon`. With `noStyle`, style the current position and interaction state through `data-corner`, `data-dragging`, and `data-snapping` on the toolbar element.
@@ -14,32 +14,37 @@ group: frameworks
14
14
  code.
15
15
 
16
16
  ```tsx title="src/consent.tsx"
17
- import type { ReactNode } from 'react';
18
17
  import {
19
- ConsentBanner,
20
- ConsentDialog,
21
- ConsentDialogLink,
22
- ConsentProvider,
23
- hosted,
18
+ ConsentBanner,
19
+ ConsentDialog,
20
+ ConsentDialogLink,
21
+ ConsentProvider,
22
+ hosted,
24
23
  } from 'c15t/react';
24
+ import type { ReactNode } from 'react';
25
+
26
+ import { scripts } from './scripts';
27
+
25
28
  import 'c15t/react/styles.css';
26
29
 
27
- const backendURL = import.meta.env.VITE_C15T_BACKEND_URL;
28
- if (!backendURL) throw new Error('Set VITE_C15T_BACKEND_URL');
29
- const mode = hosted({ url: backendURL });
30
-
31
- export function Consent({ children }: { children: ReactNode }) {
32
- return (
33
- <ConsentProvider options={{ mode }}>
34
- {children}
35
- <ConsentBanner />
36
- <ConsentDialog hideBranding />
37
- <footer>
38
- <ConsentDialogLink>Privacy settings</ConsentDialogLink>
39
- </footer>
40
- </ConsentProvider>
41
- );
42
- }
30
+ const mode = hosted({ url: 'https://your-project.inth.app' });
31
+
32
+ export const Consent = ({ children }: { children: ReactNode }) => (
33
+ <ConsentProvider options={{ mode, scripts }}>
34
+ {children}
35
+ <ConsentBanner />
36
+ <ConsentDialog />
37
+ <footer>
38
+ <ConsentDialogLink>Privacy settings</ConsentDialogLink>
39
+ </footer>
40
+ </ConsentProvider>
41
+ );
42
+ ```
43
+
44
+ To hide the "Secured by" tag in the dialog card, pass `hideBranding`:
45
+
46
+ ```tsx
47
+ <ConsentDialog hideBranding />;
43
48
  ```
44
49
 
45
50
  To open it from your own component, set the active surface:
@@ -48,41 +53,41 @@ To open it from your own component, set the active surface:
48
53
  import { useSetActiveUI } from 'c15t/react';
49
54
 
50
55
  export function PrivacyMenuItem() {
51
- const setActiveUI = useSetActiveUI();
52
- return (
53
- <button type="button" onClick={() => setActiveUI('dialog')}>
54
- Cookie preferences
55
- </button>
56
- );
56
+ const setActiveUI = useSetActiveUI();
57
+ return (
58
+ <button type="button" onClick={() => setActiveUI('dialog')}>
59
+ Cookie preferences
60
+ </button>
61
+ );
57
62
  }
58
63
  ```
59
64
 
60
65
  `ConsentDialogLink` does the same with the policy's rights exposed for
61
66
  styling; see [ConsentDialogLink](./consent-dialog-link.md). For a floating
62
67
  button, see [ConsentDialogTrigger](./consent-dialog-trigger.md). Under an IAB
63
- policy, mount `IABConsentDialog` from the [IAB guide](../iab/overview.md)
68
+ policy, mount `IABConsentDialog` from the [IAB guide](../iab.md)
64
69
  instead; this dialog stays closed for the `iab` model.
65
70
 
66
71
  ## Props
67
72
 
68
- | Prop | Type | Default | Description |
69
- | ------------------------- | ------------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
70
- | `open` | `boolean` | follows the active surface | Controls the open state. When set, Escape and the dialog's own Save, Accept all and Reject all buttons no longer close it; change the prop instead. A missing policy or an unlisted model still keeps it closed. |
71
- | `showTrigger` | `boolean \|ConsentDialogTriggerProps` | `false` | Renders a floating `ConsentDialogTrigger` next to the dialog. Pass an object to configure that trigger. |
72
- | `models` | `Model[]` | `['opt-in', 'opt-out', 'none']` | Policy models the dialog responds to. Under a model that is not listed, such as `iab`, it stays closed. |
73
- | `legalLinks` | `(keyof LegalLinks)[] \|null` | none | Which of the links configured in the provider `options.legalLinks` render after the description. Omitting the prop renders none. `null` hides them. |
74
- | `hideBranding` | `boolean` | `false` | Hides the "Secured by" tag in the card. |
75
- | `uiSource` | `string` | `'dialog'` | Source identifier recorded with saves made from this dialog. |
76
- | `scrollLock`, `trapFocus` | `boolean` | from `blocking` | Deprecated. Either one set to `false` makes the dialog non-blocking, unless the provider sets `presentation.preferences.blocking` explicitly, which wins. Prefer the provider option. |
77
- | `disableAnimation` | `boolean` | `false` | Skips the enter and exit animation of the backdrop and the card. |
78
- | `noStyle` | `boolean` | `false` | Removes the built-in styling from every part. |
73
+ | Prop | Type | Default | Description |
74
+ | ------------------------- | ------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
75
+ | `open` | `boolean` | follows the active surface | Controls the open state. When set, Escape and the dialog's own Save Settings, Accept All and Reject All buttons no longer close it; change the prop instead. A missing policy or an unlisted model still keeps it closed. |
76
+ | `showTrigger` | `boolean \|ConsentDialogTriggerProps` | `false` | Renders a floating `ConsentDialogTrigger` next to the dialog. Pass an object to configure that trigger. |
77
+ | `models` | `Model[]` | `['opt-in', 'opt-out', 'none']` | Policy models the dialog responds to. Under a model that is not listed, such as `iab`, it stays closed. |
78
+ | `legalLinks` | `(keyof LegalLinks)[] \|null` | none | Which of the links configured in the provider `options.legalLinks` render after the description. Omitting the prop renders none. `null` hides them. |
79
+ | `hideBranding` | `boolean` | `false` | Hides the "Secured by" tag in the card. |
80
+ | `uiSource` | `string` | `'dialog'` | Source identifier recorded with saves made from this dialog. |
81
+ | `scrollLock`, `trapFocus` | `boolean` | from `blocking` | Deprecated. Either one set to `false` makes the dialog non-blocking, unless the provider sets `presentation.preferences.blocking` explicitly, which wins. Prefer the provider option. |
82
+ | `disableAnimation` | `boolean` | `false` | Skips the enter and exit animation of the backdrop and the card. |
83
+ | `noStyle` | `boolean` | `false` | Removes the built-in styling from every part. |
79
84
 
80
85
  ## Behavior
81
86
 
82
87
  The dialog is a modal wrapper around the preference center. Its card has a
83
88
  header with the title `consentManagerDialog.title` and the description
84
89
  `consentManagerDialog.description` followed by the legal links, then the
85
- same category accordion and Reject all, Accept all and Save actions that
90
+ same category accordion and Reject All, Accept All and Save Settings actions that
86
91
  `ConsentWidget` renders, then the branding tag. It mounts through a portal
87
92
  into `document.body` after hydration, so it is never part of the server
88
93
  HTML.
@@ -97,14 +102,44 @@ it is `dialog`. These set that surface:
97
102
  * `useSetActiveUI()('dialog')` in your own component, or `openDialog()`
98
103
  from `useHeadlessConsentUI()` on the headless subpath.
99
104
 
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.
104
-
105
- 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
105
+ The component code is split into its own chunk, so it is not part of the
106
+ first page load. It renders nothing until that chunk is ready, and the
107
+ chunk downloads at the first of these:
108
+
109
+ * The browser's first idle period after the page's `load` event, while the
110
+ banner is shown or a `ConsentDialogLink`, `ConsentDialogTrigger`,
111
+ `ConsentGate` button or other button that opens the dialog is mounted.
112
+ This covers a visitor who taps or presses Enter with no hover or focus
113
+ first. Browsers without `requestIdleCallback`, such as Safari, wait
114
+ 200 ms after `load` instead.
115
+ * Hover or focus on a button that opens the dialog.
116
+ * The dialog opening.
117
+
118
+ Passing `open={true}` or `showTrigger` loads it on mount. A visit with
119
+ saved consent and nothing on the page that opens the dialog never
120
+ downloads it.
121
+
122
+ Idle loading is skipped when the visitor has Save-Data on
123
+ (`navigator.connection.saveData`), on a `2g` or `slow-2g` connection, and
124
+ while offline; hover and focus still load it. To load the chunk only on
125
+ hover, focus or open, set `preloadDialog: 'intent'` in the provider
126
+ options. The default is `'idle'`. With Next.js, pass it through
127
+ `ConsentRoot`'s `options` prop.
128
+
129
+ If a preload fails, for example because the visitor is offline, nothing is
130
+ cached: the next hover, focus or open tries the download again.
131
+
132
+ The dialog's CSS travels in the same chunk and is applied before the dialog
133
+ renders, so your app's `styles.css` import stays small.
134
+
135
+ Without an `open` prop, Escape closes the dialog. Save Settings, Accept All and
136
+ Reject All close it as soon as the choice is recorded in the browser, in
137
+ the same task as the click; the exit animation still plays. The request to
138
+ the backend runs afterwards and never reopens the dialog: a failed request
139
+ keeps the choice, reports through `onError` and is retried later. See
140
+ [when a choice is saved](../../../concepts/consent-state.md#when-a-choice-is-saved)
141
+ for the order of events. A draft that went stale because the policy changed keeps the dialog open
142
+ for review. With `open={true}`, these actions leave the dialog visible; the
108
143
  parent must set `open={false}` to close it. Clicking the backdrop does not
109
144
  close it. Closing discards toggles that were not saved: the next open starts
110
145
  from the recorded choice again. After a save from an uncontrolled dialog,
@@ -138,11 +173,12 @@ The panel carries `role="dialog"`, `aria-labelledby="consent-dialog-title"`
138
173
  and `aria-describedby="consent-dialog-description"`, plus `aria-modal="true"`
139
174
  while it is blocking. Its `dir` attribute follows the active language.
140
175
 
141
- While blocking, focus moves on open to the panel itself rather than to a
142
- button, so a screen reader announces the title and description first; Tab
143
- then enters the legal links and category switches, Tab and Shift+Tab wrap
144
- inside the panel, and on close focus returns to the element that opened the
145
- dialog. A non-blocking dialog manages no focus: nothing moves focus into the
176
+ While blocking, focus moves on open to the first tabbable control in the
177
+ panel, so a keyboard user sees the focus ring on a control rather than
178
+ around the whole panel; the `aria-labelledby` and `aria-describedby`
179
+ wiring means a screen reader still announces the title and description as
180
+ focus enters. Tab and Shift+Tab wrap inside the panel, and on close focus
181
+ returns to the element that opened the dialog. A non-blocking dialog manages no focus: nothing moves focus into the
146
182
  panel or back to the opener. The backdrop is `aria-hidden` and is not
147
183
  focusable.
148
184
 
@@ -186,9 +222,18 @@ for the stock structure are `dialog.root`, `dialog.container`,
186
222
  `dialog.card`, `dialog.header`, `dialog.title`, `dialog.content`,
187
223
  `dialog.overlay`, `description.dialog`, `manager.footer` and `tag.dialog`.
188
224
 
225
+ `ConsentDialog` from the package root and from the
226
+ `c15t/react/consent-dialog` subpath is the deferred component described
227
+ under Behavior, and each `ConsentDialog.<Part>` loads with the same chunk.
228
+ `c15t/react/components/consent-dialog` exports the dialog without the
229
+ deferral, plus each part as a named export (`Card`, `Header`, `Overlay`,
230
+ `Root` and the rest). Importing from that path puts the dialog's code in
231
+ the bundle of every page that imports it, so the first open needs no
232
+ download but every visitor downloads the dialog.
233
+
189
234
  The compound parts import from `c15t/react` as properties of `ConsentDialog`,
190
235
  and `ConsentWidget` from the same module. See
191
- [Styling](../styling/overview.md) for slots and tokens.
236
+ [customize](../customize.md) for slots and tokens.
192
237
 
193
238
  ## Verify
194
239
 
@@ -19,17 +19,17 @@ placeholder button opens.
19
19
  import { ConsentGate } from 'c15t/react';
20
20
 
21
21
  export function ProductVideo() {
22
- return (
23
- <ConsentGate category="marketing" className="video-frame">
24
- <iframe
25
- src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
26
- title="Product tour"
27
- loading="lazy"
28
- allowFullScreen
29
- style={{ width: '100%', aspectRatio: '16 / 9', border: 0 }}
30
- />
31
- </ConsentGate>
32
- );
22
+ return (
23
+ <ConsentGate category="marketing" className="video-frame">
24
+ <iframe
25
+ src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
26
+ title="Product tour"
27
+ loading="lazy"
28
+ allowFullScreen
29
+ style={{ width: '100%', aspectRatio: '16 / 9', border: 0 }}
30
+ />
31
+ </ConsentGate>
32
+ );
33
33
  }
34
34
  ```
35
35
 
@@ -53,8 +53,8 @@ placeholder is replaced.
53
53
  Other `div` attributes such as `className`, `style` and `ref` apply to the
54
54
  wrapper element that stays in the page in both states. `ConsentGateProps` also
55
55
  declares `noStyle` and `theme`, but the component does not apply them yet;
56
- style the wrapper with `className` and theme the placeholder through the
57
- provider `theme` option.
56
+ style the wrapper with `className`, and style the built-in placeholder
57
+ through its parts in the provider options.
58
58
 
59
59
  ## Behavior
60
60
 
@@ -70,23 +70,26 @@ children unmount and the embed disappears. `ConsentGate` does not depend on the
70
70
  network blocker or the iframe blocker modules; use those for markup you
71
71
  cannot wrap in a component.
72
72
 
73
- The built-in placeholder shows the title `frame.title`, "Accept {category}
74
- consent to view this content.", with `{category}` replaced by
75
- `consentTypes.<category>.title`, and a button labelled `frame.actionButton`,
76
- "Enable {category} consent". The button opens the preference center; it
73
+ The built-in placeholder shows the title `consentGate.title`,
74
+ `Accept {category} consent to view this content.`, with `{category}`
75
+ replaced by `consentTypes.<category>.title`, and a button labelled
76
+ `consentGate.actionButton`, `Enable {category} consent`. The button opens the
77
+ preference center; it
77
78
  does not grant the category by itself, because the visitor still has to
78
79
  save. Mount a `ConsentDialog` in the same provider so the button has
79
80
  something to open.
80
81
 
81
82
  Under a policy with `scopeMode: 'strict'`, a category outside the rule's
82
- scope cannot be granted. The built-in placeholder then shows `frame.policyBlocked`,
83
+ scope cannot be granted. The built-in placeholder then shows `consentGate.policyBlocked`,
83
84
  "This content is unavailable under your region's consent policy.", without
84
85
  the button, and a stored grant for that category stays blocked.
85
86
 
86
87
  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
88
+ server renders the placeholder for a denied category. For a granted
89
+ category it renders the empty wrapper, and the children mount once
90
+ hydration completes, so an iframe loads once even when the page streams
91
+ inside a `Suspense` boundary. A grant in the request cookie allows the
92
+ embed only when policy and privacy signals permit it. Browser privacy signals detected during hydration can
90
93
  withdraw that permission. In a browser-only setup the
91
94
  provider has no permission until it resolves policy on the client, so
92
95
  `ConsentGate` shows the placeholder first and swaps in the embed after resolution
@@ -124,8 +127,24 @@ translated text; pass children to either to replace it.
124
127
  </ConsentGate>
125
128
  ```
126
129
 
127
- The built-in placeholder carries `data-testid="frame-placeholder"` and its
128
- button `data-testid="frame-open-dialog"`.
130
+ To restyle the built-in placeholder without replacing it, set its parts
131
+ under `components['consent-gate']` in the provider options. `root` is the
132
+ card, and `title` and `button` are its text and button. The `consentGate`, `consentGateTitle` and
133
+ `consentGateButton` theme slots reach the same parts. The button part
134
+ applies on top of `button.primary`.
135
+
136
+ ```tsx
137
+ components: {
138
+ 'consent-gate': {
139
+ root: { className: 'rounded-none' },
140
+ button: { className: 'font-semibold' },
141
+ },
142
+ },
143
+ ```
144
+
145
+ The built-in placeholder carries `data-testid="consent-gate-placeholder"`,
146
+ its title `data-testid="consent-gate-title"` and its button
147
+ `data-testid="consent-gate-button"`.
129
148
 
130
149
  ## Accessibility
131
150
 
@@ -142,7 +161,9 @@ center and is not restricted by policy or privacy signals such as GPC.
142
161
  Load the page with the category denied. The placeholder text names the
143
162
  category and the network panel shows no request to the embed's host. With a
144
163
  prefetched snapshot, the server HTML contains the placeholder for a denied
145
- category or the embed for a granted category. Browser-only initialization
164
+ category and neither placeholder nor embed for a granted category; the
165
+ embed appears after hydration and the network panel shows one request for
166
+ its document. Browser-only initialization
146
167
  shows the placeholder before policy resolves, then replaces it with the
147
168
  embed if the category is granted. After policy resolves, activate the
148
169
  placeholder button: the preference center opens. Turn the category on and