@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
@@ -0,0 +1,354 @@
1
+ ---
2
+ title: Compose your own banner
3
+ description: Build a React consent banner from the ConsentBanner parts in
4
+ c15t/react, with your own markup and buttons, while the policy still decides
5
+ which actions appear.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## When to compose
10
+
11
+ Compose the banner from its parts when you need its elements in a different
12
+ order, an element it does not have, or your design system's button in place
13
+ of c15t's. The parts keep the stock
14
+ behavior. They read the same policy, record the same choices and render the
15
+ same class names, so theme tokens and slots still apply.
16
+
17
+ Try the lighter tools first:
18
+
19
+ | Change | Use |
20
+ | --------------------------------- | --------------------------------------------------------------------------------- |
21
+ | Colors, type, radius, spacing | [Theme tokens](../../customization/tokens.md) |
22
+ | Classes or attributes on one part | [Component parts](../../customization/slots.md) |
23
+ | Labels and languages | [Copy and translations](../../customization/translations.md) |
24
+ | Shape, position, button order | `ConsentBanner` props such as `variant`, `position`, `layout` and `primaryButton` |
25
+ | Different markup or order | Compound parts, on this page |
26
+ | Entirely custom UI and behavior | Headless hooks |
27
+
28
+ For a banner that shares nothing with the stock markup, use the
29
+ [headless hooks](./headless.md). For one changed class,
30
+ token or prop, see [customize](./customize.md).
31
+
32
+ ## Compose the banner
33
+
34
+ `CookieBanner` keeps the stock card, title and copy, and renders each action
35
+ the policy asks for as your design system's button. `BrandButton` stands in
36
+ for that button:
37
+
38
+ ```tsx title="src/brand-button.tsx"
39
+ import { forwardRef } from 'react';
40
+ import type { ButtonHTMLAttributes, ForwardedRef } from 'react';
41
+
42
+ interface BrandButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
43
+ tone?: 'solid' | 'outline';
44
+ }
45
+
46
+ const renderBrandButton = function renderBrandButton(
47
+ { className, tone = 'outline', type, ...props }: BrandButtonProps,
48
+ ref: ForwardedRef<HTMLButtonElement>
49
+ ) {
50
+ return (
51
+ <button
52
+ ref={ref}
53
+ {...props}
54
+ className={['brand-button', className].filter(Boolean).join(' ')}
55
+ data-tone={tone}
56
+ // Under `asChild`, c15t passes no type. Default to `button` so an
57
+ // action never submits a surrounding form.
58
+ type={type === 'submit' ? 'submit' : 'button'}
59
+ />
60
+ );
61
+ };
62
+
63
+ /**
64
+ * Your design system's button. To work under `asChild`, it forwards its ref
65
+ * and passes every other prop to the `button` element, so c15t's click
66
+ * handler, `data-action` and class names reach the DOM.
67
+ */
68
+ export const BrandButton = forwardRef(renderBrandButton);
69
+
70
+ BrandButton.displayName = 'BrandButton';
71
+ ```
72
+
73
+ ```tsx title="src/cookie-banner.tsx"
74
+ import { ConsentBanner, useTranslations } from 'c15t/react';
75
+
76
+ import { BrandButton } from './brand-button';
77
+
78
+ const actionParts = {
79
+ accept: ConsentBanner.AcceptButton,
80
+ customize: ConsentBanner.CustomizeButton,
81
+ dismiss: ConsentBanner.DismissButton,
82
+ reject: ConsentBanner.RejectButton,
83
+ } as const;
84
+
85
+ /**
86
+ * The stock card, title and copy, with each action the policy asks for
87
+ * rendered as your own button. `PolicyActions` decides which actions appear
88
+ * and in what order, so a notice still gets its acknowledgement.
89
+ */
90
+ export const CookieBanner = () => {
91
+ const { common } = useTranslations();
92
+ const labels = {
93
+ accept: common.acceptAll,
94
+ customize: common.customize,
95
+ dismiss: common.acknowledge,
96
+ reject: common.rejectAll,
97
+ };
98
+
99
+ return (
100
+ <ConsentBanner.Root>
101
+ <ConsentBanner.Card>
102
+ <ConsentBanner.Header>
103
+ <ConsentBanner.Title />
104
+ <ConsentBanner.Description />
105
+ </ConsentBanner.Header>
106
+ <ConsentBanner.PolicyActions
107
+ renderAction={(action, { key, isPrimary, ...props }) => {
108
+ if (action === 'save') {
109
+ return null;
110
+ }
111
+ const Action = actionParts[action];
112
+ return (
113
+ <Action key={key} {...props} asChild noStyle>
114
+ <BrandButton tone={isPrimary ? 'solid' : 'outline'}>
115
+ {labels[action]}
116
+ </BrandButton>
117
+ </Action>
118
+ );
119
+ }}
120
+ />
121
+ </ConsentBanner.Card>
122
+ </ConsentBanner.Root>
123
+ );
124
+ };
125
+ ```
126
+
127
+ The parts read the nearest `ConsentProvider`. In the `src/consent.tsx` file
128
+ from the [quickstart](./quickstart.md), import
129
+ `CookieBanner` from `./cookie-banner` and render it in place of
130
+ `ConsentBanner`, next to `ConsentDialog`:
131
+
132
+ ```tsx title="src/consent.tsx (partial)"
133
+ <ConsentProvider options={{ mode, scripts }}>
134
+ {children}
135
+ <CookieBanner />
136
+ <ConsentDialog />
137
+ <footer>
138
+ <ConsentDialogLink>Privacy settings</ConsentDialogLink>
139
+ </footer>
140
+ </ConsentProvider>;
141
+ ```
142
+
143
+ `ConsentBanner.Root` renders only while the banner is the active surface,
144
+ the same condition the stock `ConsentBanner` uses, so `CookieBanner` needs
145
+ no visibility logic. It renders nothing before a policy resolves and closes
146
+ once the visitor chooses.
147
+
148
+ `ConsentBanner.PolicyActions` renders the actions the policy asks for, in
149
+ the policy's order and groups, with any rights links, such as "Do not sell
150
+ or share my data", before them. For each action it calls `renderAction`
151
+ with the action name and the props c15t would give the stock button:
152
+ `key`, `consentAction`, `isPrimary`, and a `style` that stretches the
153
+ buttons when the layout fills the row. Return an element to replace that
154
+ button, or `null` to keep the stock one. The banner never renders `save`,
155
+ so the example skips it.
156
+
157
+ Each action part gets those props plus `asChild` and `noStyle`. `asChild`
158
+ renders `BrandButton` in place of c15t's `button`, and `noStyle` drops
159
+ c15t's button classes so they do not compete with yours. The part keeps its
160
+ behavior. Accept saves every category and Reject saves only necessary, and
161
+ both close the banner. Customize opens the preference dialog. Dismiss
162
+ records that the visitor saw a notice, without recording a choice.
163
+
164
+ With `asChild`, the part's default label does not render, because your
165
+ element keeps its own children. `useTranslations()` returns the labels the
166
+ stock buttons use, in the visitor's language.
167
+
168
+ Under a notice, the policy asks only for an acknowledgement, so
169
+ `PolicyActions` passes `dismiss` and `CookieBanner` renders one button. If
170
+ you place `ConsentBanner.AcceptButton` and `ConsentBanner.RejectButton`
171
+ yourself instead, they render under every policy, including one that owes
172
+ only a notice.
173
+
174
+ ## Banner parts
175
+
176
+ Every part is a property of `ConsentBanner`. All parts except `Root` and
177
+ `PolicyActions` forward their ref, and they accept the attributes of the
178
+ element they render plus `noStyle`.
179
+
180
+ | Part | Renders | Props |
181
+ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
182
+ | `Root` | The `div` that positions the banner, only while the banner shows. Renders the backdrop itself when blocking. | `variant`, `position`, `blocking`, `models` (default `['opt-in', 'opt-out']`), `uiSource` (default `'banner'`), `noStyle`, `disableAnimation` |
183
+ | `Card` | The visible `div`, labelled with the title. `role="region"`, or `role="dialog"` with `aria-modal="true"` and a focus trap when blocking. | `asChild` |
184
+ | `Header` | A `div` for the title and copy. | `asChild` |
185
+ | `Title` | An `h2` with the translated title, or the notice title under a notice. | children replace the text |
186
+ | `Description` | A `div` with the translated copy and the configured legal links. | `legalLinks`, `asChild` |
187
+ | `PolicyActions` | `Footer`, the rights links and the policy's action groups. | `renderAction`, children replace the rights links |
188
+ | `Footer` | A `div` for the actions. | `asChild` |
189
+ | `FooterSubGroup` | A `div` for one group of actions. `Actions` is an alias. | `asChild` |
190
+ | `AcceptButton` | A `button` that saves every category and closes the banner. | action button props |
191
+ | `RejectButton` | A `button` that saves only necessary and closes the banner. | action button props |
192
+ | `CustomizeButton` | A `button` that opens the preference dialog. | action button props |
193
+ | `DismissButton` | A `button` that acknowledges a notice. | action button props |
194
+ | `Rights` | A `div` with one `RightLink` per right the policy recommends, or nothing. | `rights`, `asChild` |
195
+ | `RightLink` | A `button` styled as underlined text that opens the preference dialog. | `right` (`'opt-out'` or `'preferences'`), `asChild` |
196
+ | `Overlay` | The backdrop of a blocking banner. `Root` already renders it, so do not add it. | none |
197
+
198
+ `Content` is an alias of `Card`.
199
+
200
+ The action buttons take `asChild`, `noStyle`, `consentAction`, `isPrimary`,
201
+ `variant` (`'primary'` or `'neutral'`), `mode` (`'filled'`, `'stroke'`,
202
+ `'lighter'` or `'ghost'`), `onClick` and any `button` attribute. Your
203
+ `onClick` runs before the action, and calling `event.preventDefault()` in
204
+ it skips the action. `consentAction` sets `data-action` and selects the
205
+ `theme.consentActions` entry. `PolicyActions` passes it for you, and
206
+ `DismissButton` sets it itself. A button you place yourself has no
207
+ `data-action` and no per-action theme until you pass `consentAction`.
208
+
209
+ `useConsentBannerSurface()` returns the `variant`, `position`,
210
+ `positionSource` and `blocking` that `Root` resolved, for your own elements
211
+ that depend on the banner's shape.
212
+
213
+ [ConsentBanner](./components/consent-banner.md) lists the
214
+ stock banner's props, variants and data attributes.
215
+
216
+ ## Render a part as your own element
217
+
218
+ `asChild` swaps the part's element for the one element you pass as its
219
+ child. `Card`, `Header`, `Description`, `Footer`, `FooterSubGroup`,
220
+ `Rights`, `RightLink` and the action buttons support it. `Root` and
221
+ `Overlay` ignore it, and `Title` always renders an `h2`, so a child you pass
222
+ to `Title` ends up inside that `h2`. `Description` with `asChild` drops the
223
+ inline legal links.
224
+
225
+ The part merges its props onto your element:
226
+
227
+ * The part's props apply first, and props you set on the child win.
228
+ * `className` keeps both, the part's classes first.
229
+ * `style` merges, and your values win per property.
230
+ * Event handlers both run, yours first. On an action button, calling
231
+ `event.preventDefault()` in yours skips c15t's action.
232
+ * The part's ref and your child's ref both receive the DOM node.
233
+ * Your child keeps its own children. The part's default content, such as a
234
+ button label, does not render.
235
+
236
+ Your component has to pass its ref and every prop it does not use to its
237
+ DOM element. Otherwise c15t's click handler, `data-action` and focus
238
+ handling never reach the page. The `BrandButton` in the example does both.
239
+ Pass exactly one element. Text, a fragment or several children render
240
+ nothing.
241
+
242
+ Action buttons pass no `type` under `asChild`, so give your button a
243
+ default of `type="button"`. Without it, the button submits a surrounding
244
+ form.
245
+
246
+ ## Compose the preference dialog
247
+
248
+ `ConsentDialog` has parts too. Compose it when the dialog needs a different
249
+ header or footer, and keep `ConsentWidget` inside `Content`. The widget
250
+ owns the category switches, the draft and the policy's Save, Accept and
251
+ Reject buttons.
252
+
253
+ | Part | Renders | Props |
254
+ | -------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
255
+ | `Root` | The dialog in a portal on `document.body`, with its open state, backdrop, focus trap, scroll lock and Escape to close. | `open`, `models` (default `['opt-in', 'opt-out', 'none']`), `overlay` (a node, or `false` for none), `noStyle`, `disableAnimation`, `scrollLock`, `trapFocus`, `uiSource` (default `'dialog'`) |
256
+ | `Card` | The panel `div`. | `className`, `style`, `noStyle` |
257
+ | `Header` | A `div` for the title and copy. | `asChild` |
258
+ | `HeaderTitle` | An `h2` with `id="consent-dialog-title"`. | children replace the text |
259
+ | `HeaderDescription` | A `div` with `id="consent-dialog-description"` and the legal links. | `legalLinks`, `asChild` |
260
+ | `Content` | A `div` for `ConsentWidget`. | `asChild` |
261
+ | `Footer` | A `div` with the branding tag, unless you pass children or `hideBranding`. | `hideBranding`, `asChild` |
262
+ | `ConsentCustomizationCard` | The stock card with all of the above. | `legalLinks`, `hideBranding`, `noStyle` |
263
+ | `Overlay` | The backdrop. `Root` already renders it. | none |
264
+
265
+ ```tsx
266
+ <ConsentDialog.Root>
267
+ <ConsentDialog.Card>
268
+ <ConsentDialog.Header>
269
+ <ConsentDialog.HeaderTitle>Your privacy choices</ConsentDialog.HeaderTitle>
270
+ <ConsentDialog.HeaderDescription />
271
+ </ConsentDialog.Header>
272
+ <ConsentDialog.Content>
273
+ <ConsentWidget />
274
+ </ConsentDialog.Content>
275
+ <ConsentDialog.Footer hideBranding />
276
+ </ConsentDialog.Card>
277
+ </ConsentDialog.Root>
278
+ ```
279
+
280
+ Render the composed dialog in place of `ConsentDialog`, next to your
281
+ banner. The stock `ConsentDialog` downloads its code when the dialog first
282
+ opens. The parts load that same code when they first render, and a
283
+ composed `ConsentDialog.Root` renders on every page, so the code downloads
284
+ with the page. `c15t/react/components/consent-dialog` exports the same
285
+ parts without the deferral.
286
+
287
+ `ConsentWidget` has parts as well, such as `ConsentWidget.Root`,
288
+ `ConsentWidget.Accordion`, `ConsentWidget.AccordionItems` and
289
+ `ConsentWidget.PolicyActions`, for a preference list with your own layout.
290
+
291
+ The parts come from `c15t/react`, like `ConsentDialog` and `ConsentWidget`.
292
+ [ConsentDialog](./components/consent-dialog.md) and
293
+ [ConsentWidget](./components/consent-widget.md) list every
294
+ part and its slots.
295
+
296
+ ## Keep the banner accessible
297
+
298
+ The stock parts carry the roles, labels and focus handling. A composed
299
+ banner keeps them as long as you keep the parts that own them:
300
+
301
+ * `Card` owns the banner's role, label and focus trap. With `asChild`, your
302
+ element receives them, so do not set `role` or `aria-modal` on it.
303
+ * `Card` takes its `aria-label` from the translated title, not from text you
304
+ pass to `Title`. If you change the title text, pass the same text as
305
+ `aria-label` on `Card`.
306
+ * To attach your own ref to `Card`, pass a ref object from `useRef`. A
307
+ callback ref turns off the focus trap on a blocking banner.
308
+ * Render each action as a real `button`. Its text is the accessible name,
309
+ so keep the label inside your button.
310
+ * Keep a way to reopen preferences after the banner closes, such as the
311
+ `ConsentDialogLink` in your footer.
312
+ * The dialog's `Root` names the panel with the ids on `HeaderTitle` and
313
+ `HeaderDescription`. Keep both parts, or put `id="consent-dialog-title"`
314
+ and `id="consent-dialog-description"` on your own heading and copy.
315
+
316
+ ## Keep theme tokens and slots working
317
+
318
+ The parts render the stock class names, so the c15t stylesheet and its
319
+ `--c15t-*` tokens style a composed banner the way they style the stock one.
320
+ Keep the stylesheet import.
321
+
322
+ Slots in the provider's `options.components` reach each part by key:
323
+ `banner.root`, `banner.card`, `banner.header`, `banner.title`,
324
+ `description.banner`, `banner.footer` and `banner.actions`,
325
+ `banner.actionGroup`, `banner.rights` and `banner.rightLink`. A class you
326
+ pass to a part joins its stock and slot classes. The stock `ConsentBanner`
327
+ also renders the `banner.cardShell` wrapper and the "Secured by" tag, and a
328
+ composed banner renders neither.
329
+
330
+ Action buttons read the `button.primary` or `button.secondary` slot and the
331
+ `theme.consentActions` entry for their `consentAction`. `noStyle` on a
332
+ button drops c15t's button classes and keeps slot classes. `noStyle` on
333
+ `Root` removes the built-in styling from every part inside it.
334
+
335
+ [Customize](./customize.md) shows where to set tokens,
336
+ slots and `consentActions` in a React app.
337
+
338
+ ## Check the result
339
+
340
+ 1. Clear site data and reload. The composed banner appears with your
341
+ buttons. In DevTools Elements, each action is your element with a
342
+ `data-action` attribute. In DevTools Network, no optional vendor request
343
+ has fired.
344
+ 2. Tab through the banner. Every action is reachable in the policy's order
345
+ and shows a focus ring. Press Enter on Customize, and the preference
346
+ dialog opens.
347
+ 3. Click Reject all. The banner closes. Reload, and it stays closed with
348
+ the vendors still blocked.
349
+ 4. Reopen preferences from your footer link. The optional categories are
350
+ off. Allow one, save, and only that category's vendors load.
351
+ 5. If your policies include a notice region, test it too. The banner shows
352
+ your acknowledgement button and no Accept or Reject.
353
+
354
+ [Verify consent](../../guides/verify-consent.md) has the full checklist.
@@ -0,0 +1,126 @@
1
+ ---
2
+ title: Content Security Policy
3
+ description: Run c15t in a React app under a Content Security Policy, allow the
4
+ consent backend and vendor hosts, and pass a nonce to ConsentProvider and
5
+ ConsentTheme.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## What c15t adds to the page
10
+
11
+ A React app with c15t adds these to the page, and your policy must allow them:
12
+
13
+ | What | Directive |
14
+ | ------------------------------------------------------------------------------------------ | ----------------------------------------------------- |
15
+ | `c15t/react/styles.css`, which your bundler emits with your own CSS | `style-src 'self'` |
16
+ | The `<style id="c15t-theme">` element, if you render `ConsentTheme` | `style-src` with a nonce or its hash |
17
+ | `<script>` elements the script loader creates for `scripts` entries, both `src` and inline | `script-src` with each vendor's host, or a nonce |
18
+ | Browser requests to your backend's `/init` and `/subjects` | `connect-src` with the backend origin |
19
+ | Iframes inside `ConsentGate`, and the frames, images and requests your vendors load | `frame-src`, `img-src`, `connect-src` for each vendor |
20
+
21
+ In a client-rendered app, c15t components set their inline styles through
22
+ React. A policy does not restrict styles set that way, so they need no
23
+ directive.
24
+
25
+ ## Allow the consent backend
26
+
27
+ With `hosted({ url })`, the browser calls `GET {url}/init` on every page load
28
+ and `POST {url}/subjects` when the visitor accepts, rejects or saves. Add the
29
+ backend's origin to `connect-src`:
30
+
31
+ ```txt title="Content-Security-Policy (partial)"
32
+ connect-src 'self' https://<your-backend-host>;
33
+ ```
34
+
35
+ Use the origin of the backend URL from your Inth project or self-hosted
36
+ backend. When the URL is a same-origin path such as `/api/c15t` behind your
37
+ own proxy, `'self'` covers it.
38
+
39
+ Under an IAB TCF policy the browser also loads the Global Vendor List from the
40
+ URL the policy provides. Allow that host in `connect-src` if you use the
41
+ [IAB TCF add-on](./iab.md).
42
+
43
+ ## Allow vendor scripts
44
+
45
+ The script loader adds a `<script>` element for each `scripts` entry once its
46
+ category is allowed. List each vendor's script host in `script-src`, and the
47
+ hosts its script calls in `connect-src` and `img-src`. The vendor's own
48
+ documentation lists them. Scripts that a vendor script adds itself, such as a
49
+ tag manager's tags, need their own hosts too, or `'strict-dynamic'` with a
50
+ nonce.
51
+
52
+ A `scripts` entry with inline `textContent` instead of `src` needs a nonce or
53
+ its hash in `script-src`.
54
+
55
+ ## Use a nonce
56
+
57
+ A nonce protects a page only when it changes on every response. A static host
58
+ serves the same file to everyone, so use host allowlists there. Use a nonce
59
+ when a server renders or rewrites your `index.html` per request.
60
+
61
+ Pass the nonce as `options.nonce` on `ConsentProvider`. The script loader
62
+ stamps it on every `<script>` it creates. A `nonce` on a single `scripts`
63
+ entry takes precedence for that element.
64
+
65
+ With Vite, set `html.cspNonce` to a placeholder in `vite.config.ts`, and have
66
+ your server replace it with a new nonce on every response. Vite then adds
67
+ `<meta property="csp-nonce" nonce="...">` to the page, and the browser keeps the
68
+ value in the tag's `nonce` property. Read it where you create the provider, in
69
+ the quickstart's `src/consent.tsx`:
70
+
71
+ ```tsx title="src/consent.tsx (partial)"
72
+ const nonce =
73
+ document.querySelector<HTMLMetaElement>('meta[property="csp-nonce"]')
74
+ ?.nonce || undefined;
75
+
76
+ <ConsentProvider options={{ mode, scripts, nonce }}>
77
+ ```
78
+
79
+ If you render `ConsentTheme`, pass it the same value:
80
+ `<ConsentTheme theme={theme} nonce={nonce} />`.
81
+
82
+ The provider reads `options.nonce` once, when it mounts. A server-rendered
83
+ React app, such as one built with React Router framework mode, reads the nonce
84
+ from its own request and passes it the same way.
85
+
86
+ ## Allow the theme style element
87
+
88
+ `ConsentTheme` renders your theme tokens into a `<style id="c15t-theme">`
89
+ element. Without a nonce, a `style-src` that lacks `'unsafe-inline'` blocks it,
90
+ and the banner falls back to the default theme. The element's content depends
91
+ only on the theme, so a hash works:
92
+
93
+ 1. Load the page with the policy enforced.
94
+ 2. Copy the `sha256-...` value from the `Refused to apply inline style`
95
+ console error.
96
+ 3. Add it to `style-src` and redeploy.
97
+
98
+ Recompute the hash when you change the theme. To avoid the hash, set the
99
+ tokens in your own stylesheet instead, as
100
+ [customize](./customize.md) shows.
101
+
102
+ ## Allow embeds and the dev server
103
+
104
+ Add the host of every iframe you gate to `frame-src`. A YouTube player inside
105
+ `ConsentGate` needs `https://www.youtube-nocookie.com`, for example.
106
+
107
+ Vite's dev server injects CSS as `<style>` elements, and the DevTools panel
108
+ adds a `<style>` element without a nonce. Test the policy against a
109
+ production build.
110
+
111
+ ## Verify
112
+
113
+ Build the app, serve it with the policy enforced, not in report-only mode, and
114
+ open DevTools. Test under a policy that asks for a choice, such as an EU
115
+ opt-in policy.
116
+
117
+ 1. The banner renders with its theme colors, and the console shows no
118
+ `Refused to` error that mentions a c15t element, your backend or a vendor.
119
+ 2. Before a choice, the Network tab shows no vendor requests.
120
+ 3. Click **Accept All**. The vendor scripts load, and their `<script>` elements have ids
121
+ starting `c15t-`. With a nonce, read each element's `nonce` property in the
122
+ console. Browsers hide the attribute from `getAttribute`.
123
+ 4. The save to `/subjects` succeeds. A `connect-src` violation here means the
124
+ backend origin is missing.
125
+ 5. Reload. The banner stays closed, and the vendors load without a console
126
+ error.