@c15t/react 2.0.0-rc.7 → 2.0.0-rc.9

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 (138) hide show
  1. package/README.md +9 -0
  2. package/dist/components/consent-banner/consent-banner.cjs +1 -1
  3. package/dist/components/consent-banner/consent-banner.js +1 -1
  4. package/dist/components/consent-banner/index.cjs +1 -1
  5. package/dist/components/consent-banner/index.js +1 -1
  6. package/dist/components/consent-banner/policy-actions.cjs +1 -0
  7. package/dist/components/consent-banner/policy-actions.js +1 -0
  8. package/dist/components/consent-dialog/atoms/card.cjs +1 -1
  9. package/dist/components/consent-dialog/atoms/card.js +1 -1
  10. package/dist/components/consent-dialog-trigger/atoms/icon.cjs +1 -1
  11. package/dist/components/consent-dialog-trigger/atoms/icon.js +1 -1
  12. package/dist/components/consent-widget/atoms/footer.cjs +1 -1
  13. package/dist/components/consent-widget/atoms/footer.js +1 -1
  14. package/dist/components/consent-widget/consent-widget.cjs +1 -1
  15. package/dist/components/consent-widget/consent-widget.js +1 -1
  16. package/dist/components/consent-widget/index.cjs +1 -1
  17. package/dist/components/consent-widget/index.js +1 -1
  18. package/dist/components/consent-widget/policy-actions.cjs +1 -0
  19. package/dist/components/consent-widget/policy-actions.js +1 -0
  20. package/dist/components/iab-consent-banner/iab-consent-banner.cjs +1 -1
  21. package/dist/components/iab-consent-banner/iab-consent-banner.js +1 -1
  22. package/dist/components/iab-consent-dialog/iab-consent-dialog.cjs +1 -1
  23. package/dist/components/iab-consent-dialog/iab-consent-dialog.js +1 -1
  24. package/dist/components/shared/policy-actions.cjs +1 -0
  25. package/dist/components/shared/policy-actions.js +1 -0
  26. package/dist/components/shared/ui/branding.cjs +1 -0
  27. package/dist/components/shared/ui/branding.js +1 -0
  28. package/dist/components/shared/ui/logo.cjs +1 -1
  29. package/dist/components/shared/ui/logo.js +1 -1
  30. package/dist/components/shared/ui/switch/index.cjs +1 -1
  31. package/dist/components/shared/ui/switch/index.js +1 -1
  32. package/dist/hooks/index.cjs +1 -1
  33. package/dist/hooks/use-consent-manager.cjs +1 -1
  34. package/dist/hooks/use-consent-manager.js +1 -1
  35. package/dist/hooks/use-headless-consent-ui.cjs +1 -1
  36. package/dist/hooks/use-headless-consent-ui.js +1 -1
  37. package/dist/iab/styles.css +11 -1
  38. package/dist/iab/styles.tw3.css +14 -1
  39. package/dist/index.cjs +1 -1
  40. package/dist/providers/consent-manager-provider.cjs +1 -1
  41. package/dist/providers/consent-manager-provider.js +1 -1
  42. package/dist/server/fetch-ssr-data.cjs +1 -1
  43. package/dist/server/fetch-ssr-data.js +1 -1
  44. package/dist/styles.css +10 -1
  45. package/dist/styles.tw3.css +13 -1
  46. package/dist/version.cjs +1 -1
  47. package/dist/version.js +1 -1
  48. package/dist-types/components/consent-banner/atoms/root.d.ts +1 -1
  49. package/dist-types/components/consent-banner/consent-banner.d.ts +7 -2
  50. package/dist-types/components/consent-banner/index.d.ts +3 -0
  51. package/dist-types/components/consent-banner/policy-actions.d.ts +11 -0
  52. package/dist-types/components/consent-dialog/atoms/card.d.ts +7 -2
  53. package/dist-types/components/consent-dialog/atoms/root.d.ts +1 -1
  54. package/dist-types/components/consent-dialog/consent-dialog.d.ts +1 -1
  55. package/dist-types/components/consent-dialog-trigger/atoms/icon.d.ts +1 -1
  56. package/dist-types/components/consent-dialog-trigger/types.d.ts +1 -1
  57. package/dist-types/components/consent-widget/index.d.ts +3 -0
  58. package/dist-types/components/consent-widget/policy-actions.d.ts +11 -0
  59. package/dist-types/components/frame/atoms.d.ts +1 -1
  60. package/dist-types/components/frame/types.d.ts +1 -1
  61. package/dist-types/components/iab-consent-banner/atoms/root.d.ts +1 -1
  62. package/dist-types/components/iab-consent-banner/iab-consent-banner.d.ts +1 -1
  63. package/dist-types/components/iab-consent-dialog/atoms/root.d.ts +1 -1
  64. package/dist-types/components/iab-consent-dialog/atoms/vendor-list.d.ts +1 -1
  65. package/dist-types/components/iab-consent-dialog/iab-consent-dialog.d.ts +1 -1
  66. package/dist-types/components/shared/policy-actions.d.ts +29 -0
  67. package/dist-types/components/shared/primitives/button.d.ts +1 -1
  68. package/dist-types/components/shared/primitives/button.types.d.ts +1 -1
  69. package/dist-types/components/shared/primitives/legal-links/index.d.ts +1 -1
  70. package/dist-types/components/shared/ui/accordion/accordion.d.ts +2 -2
  71. package/dist-types/components/shared/ui/accordion/accordion.types.d.ts +1 -1
  72. package/dist-types/components/shared/ui/branding.d.ts +23 -0
  73. package/dist-types/components/shared/ui/button/button.d.ts +1 -1
  74. package/dist-types/components/shared/ui/logo.d.ts +8 -0
  75. package/dist-types/components/shared/ui/preference-item/preference-item.d.ts +1 -1
  76. package/dist-types/components/shared/ui/switch/index.d.ts +1 -1
  77. package/dist-types/components/shared/ui/switch/switch.d.ts +1 -1
  78. package/dist-types/components/shared/ui/switch/switch.types.d.ts +1 -1
  79. package/dist-types/components/shared/ui/tabs/tabs.d.ts +1 -1
  80. package/dist-types/context/consent-manager-context.d.ts +1 -1
  81. package/dist-types/headless.d.ts +1 -1
  82. package/dist-types/hooks/use-component-config.d.ts +1 -1
  83. package/dist-types/hooks/use-consent-manager.d.ts +1 -1
  84. package/dist-types/hooks/use-headless-consent-ui.d.ts +1 -1
  85. package/dist-types/hooks/use-ssr-status.d.ts +2 -1
  86. package/dist-types/hooks/use-translations.d.ts +1 -1
  87. package/dist-types/index.d.ts +2 -2
  88. package/dist-types/server/fetch-ssr-data.d.ts +1 -1
  89. package/dist-types/server/init-cache-key.d.ts +1 -1
  90. package/dist-types/server/types.d.ts +1 -1
  91. package/dist-types/types/consent-manager.d.ts +1 -1
  92. package/dist-types/types/theme/style-keys.d.ts +1 -1
  93. package/dist-types/types/theme/style-types.d.ts +2 -2
  94. package/dist-types/utils/cn.d.ts +1 -1
  95. package/dist-types/utils/theme-utils.d.ts +1 -1
  96. package/dist-types/version.d.ts +1 -1
  97. package/docs/building-headless-components.md +40 -22
  98. package/docs/callbacks.md +76 -9
  99. package/docs/components/consent-banner.md +83 -9
  100. package/docs/components/consent-dialog.md +12 -2
  101. package/docs/components/consent-manager-provider.md +3 -1
  102. package/docs/components/consent-widget.md +61 -8
  103. package/docs/concepts/client-modes.md +16 -4
  104. package/docs/concepts/initialization-flow.md +9 -2
  105. package/docs/concepts/policy-packs.md +2 -2
  106. package/docs/hooks/use-consent-manager/overview.md +17 -3
  107. package/docs/hooks/use-ssr-status.md +1 -1
  108. package/docs/hooks/use-translations.md +1 -0
  109. package/docs/iab/consent-banner.md +2 -5
  110. package/docs/iab/consent-dialog.md +3 -6
  111. package/docs/iab/overview.md +11 -5
  112. package/docs/integrations/building-integrations.md +405 -0
  113. package/docs/integrations/databuddy.md +22 -5
  114. package/docs/integrations/google-tag-manager.md +2 -2
  115. package/docs/integrations/google-tag.md +2 -29
  116. package/docs/integrations/linkedin-insights.md +1 -1
  117. package/docs/integrations/meta-pixel.md +1 -1
  118. package/docs/integrations/microsoft-uet.md +1 -1
  119. package/docs/integrations/overview.md +18 -2
  120. package/docs/integrations/posthog.md +39 -17
  121. package/docs/integrations/tiktok-pixel.md +1 -1
  122. package/docs/integrations/x-pixel.md +1 -1
  123. package/docs/optimization.md +2 -6
  124. package/docs/policy-packs.md +7 -7
  125. package/docs/quickstart.md +11 -5
  126. package/docs/script-loader.md +22 -1
  127. package/docs/styling/tailwind.md +23 -17
  128. package/iab/styles.css +1 -0
  129. package/package.json +13 -11
  130. package/readme.json +6 -0
  131. package/src/iab/styles.css +11 -0
  132. package/src/iab/styles.tw3.css +8 -4
  133. package/src/styles.css +10 -0
  134. package/src/styles.tw3.css +7 -4
  135. package/styles.css +1 -0
  136. package/dist/components/shared/libs/policy-actions.cjs +0 -1
  137. package/dist/components/shared/libs/policy-actions.js +0 -1
  138. package/dist-types/components/shared/libs/policy-actions.d.ts +0 -26
@@ -2,18 +2,27 @@
2
2
  title: Building Headless Components
3
3
  description: Build policy-aware custom consent components in React using the headless hooks and policy-pack tooling.
4
4
  ---
5
- Building headless components is easier now because c15t exposes policy-aware primitives instead of forcing you to reconstruct banner rules by hand.
5
+ Building custom consent UI is easier now because c15t exposes multiple layers of policy-aware primitives instead of forcing you to reconstruct banner rules by hand.
6
+
7
+ The layering is:
8
+
9
+ * stock component props for the shortest path
10
+ * `ConsentBanner.PolicyActions` and `ConsentWidget.PolicyActions` for custom structure with policy-aware actions
11
+ * `useHeadlessConsentUI()` for fully manual action rendering and non-standard controls
6
12
 
7
13
  > ⚠️ **Warning:**
8
14
  > Headless is the last step in the customization ladder. Use this guide only when pre-built components, tokens, slots, compound components, and noStyle are no longer sufficient.
9
15
 
10
- The headless stack is:
16
+ The headless stack underneath that is:
11
17
 
12
18
  * `useHeadlessConsentUI()` for policy-aware banner/dialog actions, ordering, layout, and primary actions hints
19
+ * `@c15t/ui/utils` for the pure policy-action helpers that framework packages build on
13
20
  * `useConsentManager()` for runtime state, categories, selected consent state, and policy metadata
14
21
  * `useTranslations()` for the resolved copy
15
22
  * `offlinePolicy.policyPacks` for offline previews that behave like backend policy resolution
16
23
 
24
+ The split is intentional: `@c15t/ui` owns pure policy-action resolution, while the framework hooks own visibility, consent mutations, and reactive state.
25
+
17
26
  > ℹ️ **Info:**
18
27
  > This guide is about building your own components while still respecting resolved policy-pack behavior. For the general headless overview, see Headless Mode.
19
28
 
@@ -33,6 +42,11 @@ The main win is that your custom UI can stay aligned with policy packs without d
33
42
 
34
43
  That means your component mostly focuses on markup and design-system concerns instead of re-implementing policy interpretation.
35
44
 
45
+ For most compound-component layouts, start with `ConsentBanner.PolicyActions` or `ConsentWidget.PolicyActions`. They render stock c15t buttons and translations by default, and `renderAction` is only needed when you want to override the action mapping. Reach for manual `actionGroups` mapping when you need action rendering that no longer fits the stock button compounds.
46
+
47
+ > ℹ️ **Info:**
48
+ > For custom layouts built from c15t compound components, prefer ConsentBanner.PolicyActions and ConsentWidget.PolicyActions. The examples below intentionally use manual actionGroups mapping to show the fully headless escape hatch.
49
+
36
50
  ## Provider Setup for Local Policy Testing
37
51
 
38
52
  ```tsx
@@ -168,25 +182,29 @@ export function CustomConsentDialog() {
168
182
  ))}
169
183
  </div>
170
184
 
171
- <div className="mt-4 flex gap-2">
172
- {dialog.orderedActions.map((action) => (
173
- <button
174
- key={action}
175
- type="button"
176
- onClick={() => {
177
- if (action === 'customize') {
178
- void saveCustomPreferences();
179
- return;
180
- }
181
- void performDialogAction(action);
182
- }}
183
- >
184
- {action === 'accept'
185
- ? translations.common.acceptAll
186
- : action === 'reject'
187
- ? translations.common.rejectAll
188
- : translations.common.save}
189
- </button>
185
+ <div className="mt-4 space-y-2">
186
+ {dialog.actionGroups.map((group, index) => (
187
+ <div key={`${group.join('-')}-${index}`} className="flex gap-2">
188
+ {group.map((action) => (
189
+ <button
190
+ key={action}
191
+ type="button"
192
+ onClick={() => {
193
+ if (action === 'customize') {
194
+ void saveCustomPreferences();
195
+ return;
196
+ }
197
+ void performDialogAction(action);
198
+ }}
199
+ >
200
+ {action === 'accept'
201
+ ? translations.common.acceptAll
202
+ : action === 'reject'
203
+ ? translations.common.rejectAll
204
+ : translations.common.save}
205
+ </button>
206
+ ))}
207
+ </div>
190
208
  ))}
191
209
  </div>
192
210
  </section>
@@ -199,7 +217,7 @@ export function CustomConsentDialog() {
199
217
  When you build custom banner or dialog components, make sure they use:
200
218
 
201
219
  * `activeUI` or `banner.isVisible` / `dialog.isVisible` for visibility
202
- * `allowedActions`, `orderedActions`, or `actionGroups` instead of hard-coding buttons
220
+ * `allowedActions`, `actionGroups`, and `primaryActions` instead of hard-coding buttons
203
221
  * `primaryActions` for visual emphasis
204
222
  * `consentCategories` when deciding which category toggles to render
205
223
  * `policyDecision` when you want to debug why a specific UI state was chosen
package/docs/callbacks.md CHANGED
@@ -2,7 +2,12 @@
2
2
  title: Callbacks
3
3
  description: React to consent lifecycle events - initialization, consent changes, errors, and revocation reloads.
4
4
  ---
5
- Callbacks let you run custom code at key points in the consent lifecycle. Define them in the provider's `callbacks` option, or register them dynamically via `useConsentManager()`.
5
+ Callbacks let you run custom code at key points in the consent lifecycle. Define them in the provider or runtime `callbacks` option, or register them dynamically after initialization.
6
+
7
+ For analytics SDKs and other change-only integrations, prefer `subscribeToConsentChanges()` or `onConsentChanged`. Use `onConsentSet` when you want the broader lifecycle signal, including initialization, automatic defaults, and replay-aware registration.
8
+
9
+ > ℹ️ **Info:**
10
+ > subscribeToConsentChanges() is the recommended API for analytics SDKs and consent-mode integrations. It only emits future saves that actually changed persisted preferences.
6
11
 
7
12
  ## Configuration
8
13
 
@@ -23,8 +28,10 @@ export function ConsentManager({ children }: { children: ReactNode }) {
23
28
  console.log('Language:', translations.language);
24
29
  },
25
30
  onConsentSet: ({ preferences }) => {
26
- // Fire analytics event
27
- analytics.track('consent_updated', { preferences });
31
+ console.log('Consent lifecycle event:', preferences);
32
+ },
33
+ onConsentChanged: ({ allowedCategories, deniedCategories }) => {
34
+ analytics.syncConsent({ allowedCategories, deniedCategories });
28
35
  },
29
36
  onError: ({ error }) => {
30
37
  errorReporter.captureMessage(error);
@@ -42,6 +49,18 @@ export function ConsentManager({ children }: { children: ReactNode }) {
42
49
  }
43
50
  ```
44
51
 
52
+ ## Choose the Right Surface
53
+
54
+ |Surface|Replays when registered late?|Fires on init / hydration / auto-grants?|Best for|
55
+ |--|--|--|--|
56
+ |`onBannerFetched`|Yes, via `setCallback('onBannerFetched', ...)` after init|Yes|Logging resolved policy, location, and translations|
57
+ |`onConsentSet`|Yes, via `setCallback('onConsentSet', ...)`|Yes|Broad lifecycle hooks, debugging, and integrations that want the latest full state regardless of how it was reached|
58
+ |`onConsentChanged`|No|No|Declarative change-only integrations|
59
+ |`subscribeToConsentChanges()`|No|No|Canonical change-only subscriptions after mount|
60
+
61
+ > ℹ️ **Info:**
62
+ > Script.onConsentChange is a script-scoped lifecycle hook. It is not the global consent change API for analytics SDKs or other app-wide integrations.
63
+
45
64
  ## Available Callbacks
46
65
 
47
66
  ### `onBannerFetched`
@@ -58,14 +77,34 @@ onBannerFetched: ({ jurisdiction, location, translations }) => {
58
77
 
59
78
  ### `onConsentSet`
60
79
 
61
- Called whenever consent preferences are saved - whether by user action (`saveConsents()`), automatic defaults, or on initialization when no jurisdiction is detected.
80
+ Called whenever c15t broadly settles consent state: store initialization, automatic defaults during init, explicit saves, and replay via `setCallback('onConsentSet', ...)`.
62
81
 
63
82
  ```tsx
64
83
  onConsentSet: ({ preferences }) => {
65
84
  // preferences: { necessary: true, measurement: true, marketing: false, ... }
66
- if (preferences.measurement) {
67
- loadAnalytics();
68
- }
85
+ console.log('Latest consent state:', preferences);
86
+ }
87
+ ```
88
+
89
+ ### `onConsentChanged`
90
+
91
+ Called only after an explicit `saveConsents()` or `setConsent()` that actually changes the saved consent state. It never fires on store creation, hydration, automatic grants, unchanged saves, or `setCallback('onConsentChanged', ...)`.
92
+
93
+ ```tsx
94
+ onConsentChanged: ({
95
+ preferences,
96
+ previousPreferences,
97
+ allowedCategories,
98
+ deniedCategories,
99
+ previousAllowedCategories,
100
+ previousDeniedCategories,
101
+ }) => {
102
+ analytics.syncConsent({
103
+ allowedCategories,
104
+ deniedCategories,
105
+ previousAllowedCategories,
106
+ previousDeniedCategories,
107
+ });
69
108
  }
70
109
  ```
71
110
 
@@ -91,7 +130,28 @@ onBeforeConsentRevocationReload: ({ preferences }) => {
91
130
  }
92
131
  ```
93
132
 
94
- ## Dynamic Registration
133
+ ## Change-Only Subscriptions
134
+
135
+ Use `subscribeToConsentChanges()` when you want a stable listener for real preference changes after mount:
136
+
137
+ ```tsx
138
+ import { useEffect } from 'react';
139
+ import { useConsentManager } from '@c15t/react';
140
+
141
+ function ConsentAnalytics() {
142
+ const { subscribeToConsentChanges } = useConsentManager();
143
+
144
+ useEffect(() => {
145
+ return subscribeToConsentChanges(({ allowedCategories, deniedCategories }) => {
146
+ analytics.syncConsent({ allowedCategories, deniedCategories });
147
+ });
148
+ }, [subscribeToConsentChanges]);
149
+
150
+ return null;
151
+ }
152
+ ```
153
+
154
+ ## Runtime Callback Registration
95
155
 
96
156
  Register or update callbacks at runtime using `setCallback()`:
97
157
 
@@ -103,11 +163,16 @@ function ConsentAnalytics() {
103
163
  const { setCallback } = useConsentManager();
104
164
 
105
165
  useEffect(() => {
166
+ setCallback('onBannerFetched', ({ jurisdiction, location }) => {
167
+ console.log('Resolved init data:', { jurisdiction, location });
168
+ });
169
+
106
170
  setCallback('onConsentSet', ({ preferences }) => {
107
- analytics.track('consent_changed', preferences);
171
+ console.log('Broad consent lifecycle event:', preferences);
108
172
  });
109
173
 
110
174
  return () => {
175
+ setCallback('onBannerFetched', undefined);
111
176
  setCallback('onConsentSet', undefined);
112
177
  };
113
178
  }, [setCallback]);
@@ -115,3 +180,5 @@ function ConsentAnalytics() {
115
180
  return null;
116
181
  }
117
182
  ```
183
+
184
+ `setCallback('onConsentSet', ...)` immediately replays the current consent state. For change-only logic, prefer `subscribeToConsentChanges()` or `onConsentChanged`.
@@ -166,7 +166,7 @@ Direct text props such as `title`, `description`, and `acceptButtonText` are sti
166
166
 
167
167
  ## Advanced: Compound Components
168
168
 
169
- Use compound components only when the stock banner structure is no longer enough and you need to rearrange existing c15t primitives:
169
+ Use compound components only when the stock banner structure is no longer enough and you need to rearrange existing c15t primitives while keeping policy-driven action grouping and emphasis:
170
170
 
171
171
  ```tsx
172
172
  <ConsentBanner.Root>
@@ -176,13 +176,7 @@ Use compound components only when the stock banner structure is no longer enough
176
176
  <ConsentBanner.Title />
177
177
  <ConsentBanner.Description />
178
178
  </ConsentBanner.Header>
179
- <ConsentBanner.Footer>
180
- <ConsentBanner.FooterSubGroup>
181
- <ConsentBanner.RejectButton />
182
- <ConsentBanner.AcceptButton />
183
- </ConsentBanner.FooterSubGroup>
184
- <ConsentBanner.CustomizeButton />
185
- </ConsentBanner.Footer>
179
+ <ConsentBanner.PolicyActions />
186
180
  </ConsentBanner.Card>
187
181
  </ConsentBanner.Root>
188
182
  ```
@@ -190,8 +184,9 @@ Use compound components only when the stock banner structure is no longer enough
190
184
  * `ConsentBanner.Root` — Outermost container, provides theme context
191
185
  * `ConsentBanner.Card` — Main content card with optional focus trapping
192
186
  * `ConsentBanner.Header` — Contains title and description
193
- * `ConsentBanner.Title` — Heading, defaults to translation `cookieBanner.title`
187
+ * `ConsentBanner.Title` — Heading, defaults to translation `consentBanner.title`
194
188
  * `ConsentBanner.Description` — Description text, supports `legalLinks` prop
189
+ * `ConsentBanner.PolicyActions` — Renders policy-aware grouped actions inside the banner footer
195
190
  * `ConsentBanner.Footer` — Action buttons container
196
191
  * `ConsentBanner.FooterSubGroup` — Groups related buttons together
197
192
  * `ConsentBanner.RejectButton` — Rejects all consent
@@ -199,6 +194,84 @@ Use compound components only when the stock banner structure is no longer enough
199
194
  * `ConsentBanner.AcceptButton` — Accepts all consent
200
195
  * `ConsentBanner.Overlay` — Optional backdrop overlay
201
196
 
197
+ For a fixed layout that intentionally ignores policy grouping, render the footer manually:
198
+
199
+ ```tsx
200
+ <ConsentBanner.Root>
201
+ <ConsentBanner.Card>
202
+ <ConsentBanner.Header>
203
+ <ConsentBanner.Title />
204
+ <ConsentBanner.Description />
205
+ </ConsentBanner.Header>
206
+ <ConsentBanner.Footer>
207
+ <ConsentBanner.FooterSubGroup>
208
+ <ConsentBanner.RejectButton />
209
+ <ConsentBanner.AcceptButton />
210
+ </ConsentBanner.FooterSubGroup>
211
+ <ConsentBanner.CustomizeButton />
212
+ </ConsentBanner.Footer>
213
+ </ConsentBanner.Card>
214
+ </ConsentBanner.Root>
215
+ ```
216
+
217
+ ## Using `renderAction` with c15t Defaults
218
+
219
+ `ConsentBanner.PolicyActions` renders stock c15t buttons and translations by default.
220
+
221
+ ```tsx
222
+ <ConsentBanner.PolicyActions />
223
+ ```
224
+
225
+ `renderAction` is optional. When you want custom mapping but still want the built-in c15t button behavior and copy, return the stock button compounds:
226
+
227
+ ```tsx
228
+ <ConsentBanner.PolicyActions
229
+ renderAction={(action, props) => {
230
+ const { key, ...buttonProps } = props
231
+
232
+ switch (action) {
233
+ case 'accept':
234
+ return <ConsentBanner.AcceptButton key={key} {...buttonProps} />
235
+ case 'reject':
236
+ return <ConsentBanner.RejectButton key={key} {...buttonProps} />
237
+ case 'customize':
238
+ return <ConsentBanner.CustomizeButton key={key} {...buttonProps} />
239
+ }
240
+ }}
241
+ />
242
+ ```
243
+
244
+ Use `useTranslations()` only when you are replacing the button markup entirely:
245
+
246
+ ```tsx
247
+ import { ConsentBanner, useTranslations } from '@c15t/react';
248
+
249
+ export function CustomBannerActions() {
250
+ const { common } = useTranslations();
251
+
252
+ return (
253
+ <ConsentBanner.PolicyActions
254
+ renderAction={(action, props) => (
255
+ <button
256
+ key={props.key}
257
+ type="button"
258
+ className={props.isPrimary ? 'btn-primary' : 'btn-secondary'}
259
+ style={props.style}
260
+ >
261
+ {action === 'accept'
262
+ ? common.acceptAll
263
+ : action === 'reject'
264
+ ? common.rejectAll
265
+ : common.customize}
266
+ </button>
267
+ )}
268
+ />
269
+ );
270
+ }
271
+ ```
272
+
273
+ For maximum control, use `useHeadlessConsentUI()` and render `banner.actionGroups` manually.
274
+
202
275
  If you only need styling changes, stay with tokens and slots instead of rebuilding the banner layout.
203
276
 
204
277
  ## Props
@@ -217,6 +290,7 @@ If you only need styling changes, stay with tokens and slots instead of rebuildi
217
290
  |trapFocus|boolean \|undefined|When true, the consent banner will trap focus|true|Optional|
218
291
  |disableAnimation|boolean \|undefined|When true, disables the entrance/exit animations|false|Optional|
219
292
  |legalLinks|(keyof LegalLinksTranslations)\[] \|null \|undefined|Controls which legal links to display. Options: \`undefined\` (default): Shows all available legal links; \`null\`: Explicitly hides all legal links; Array of keys: Shows only the specified legal links|-|Optional|
293
+ |hideBranding|boolean \|undefined|When true, hides the branding tag on the banner.|false|Optional|
220
294
  |layout|ConsentBannerLayout \|undefined|Defines the layout of buttons in the footer. Allows reordering and grouping of buttons.|-|Optional|
221
295
  |direction|PolicyUiActionDirection \|undefined|Defines how footer button groups flow.|-|Optional|
222
296
  |primaryButton|ConsentBannerButton \|undefined|Specifies which button(s) should be highlighted as the primary action.|-|Optional|
@@ -110,7 +110,7 @@ Dialog copy should be changed through `ConsentManagerProvider.options.i18n`, not
110
110
 
111
111
  ## Advanced: Compound Components
112
112
 
113
- Use compound components only when you need custom dialog markup while still keeping c15t primitives:
113
+ Use compound components only when you need custom dialog markup while still keeping c15t primitives and policy-aware footer actions:
114
114
 
115
115
  ```tsx
116
116
  <ConsentDialog.Root>
@@ -121,7 +121,12 @@ Use compound components only when you need custom dialog markup while still keep
121
121
  <ConsentDialog.HeaderDescription />
122
122
  </ConsentDialog.Header>
123
123
  <ConsentDialog.Content>
124
- <ConsentWidget />
124
+ <ConsentWidget.Root>
125
+ <ConsentWidget.Accordion type="single">
126
+ <ConsentWidget.AccordionItems />
127
+ </ConsentWidget.Accordion>
128
+ <ConsentWidget.PolicyActions />
129
+ </ConsentWidget.Root>
125
130
  </ConsentDialog.Content>
126
131
  <ConsentDialog.Footer />
127
132
  </ConsentDialog.Card>
@@ -136,6 +141,7 @@ Use compound components only when you need custom dialog markup while still keep
136
141
  * `ConsentDialog.Content` — Main content area (typically contains `ConsentWidget`)
137
142
  * `ConsentDialog.Footer` — Footer with optional branding (`hideBranding` prop)
138
143
  * `ConsentDialog.Overlay` — Backdrop overlay
144
+ * `ConsentWidget.PolicyActions` — Renders policy-aware grouped dialog actions
139
145
 
140
146
  For a quick pre-composed layout, use the shorthand card:
141
147
 
@@ -145,6 +151,10 @@ For a quick pre-composed layout, use the shorthand card:
145
151
  </ConsentDialog.Root>
146
152
  ```
147
153
 
154
+ `ConsentWidget.PolicyActions` uses stock c15t widget buttons and translations by default. Pass `renderAction` only when you need to customize the action mapping, and return stock widget button compounds if you want to preserve built-in behavior and copy.
155
+
156
+ For fully manual control over dialog action rendering, use `useHeadlessConsentUI()` and map `dialog.actionGroups` yourself.
157
+
148
158
  If the stock dialog structure still works, prefer tokens, slots, and provider configuration instead.
149
159
 
150
160
  ## Props
@@ -54,6 +54,7 @@ Event callbacks for consent actions.
54
54
  |:--|:--|:--|:--|:--:|
55
55
  |onBannerFetched|Callback\<OnBannerFetchedPayload> \|undefined|Called when the consent banner is fetched.|-|Optional|
56
56
  |onConsentSet|Callback\<OnConsentSetPayload> \|undefined|Called when the consent is set.|-|Optional|
57
+ |onConsentChanged|Callback\<OnConsentChangedPayload> \|undefined|Called only when an explicit consent save changes the previously saved consent state.|-|Optional|
57
58
  |onError|Callback\<OnErrorPayload> \|undefined|Called when an error occurs.|-|Optional|
58
59
  |onBeforeConsentRevocationReload|Callback\<OnConsentSetPayload> \|undefined|Called before the page reloads when consent is revoked.|-|Optional|
59
60
 
@@ -325,7 +326,7 @@ In hosted mode (recommended), the backend resolves the correct policy automatica
325
326
 
326
327
  ### Fallback: Offline Policies
327
328
 
328
- When no backend is available, `ConsentManagerProvider` accepts `offlinePolicy.policyPacks` for local policy resolution:
329
+ When no backend is available, `ConsentManagerProvider` accepts `offlinePolicy.policyPacks` for local policy resolution during development, testing, previews, or temporary backend outages:
329
330
 
330
331
  ```tsx
331
332
  <ConsentManagerProvider
@@ -376,6 +377,7 @@ When no backend is available, `ConsentManagerProvider` accepts `offlinePolicy.po
376
377
  Notes:
377
378
 
378
379
  * `offlinePolicy` is only used in `offline` mode.
380
+ * Treat offline policies as a development/testing tool or resilience fallback, not the primary production source of truth.
379
381
  * `offlinePolicy.i18n` lets offline mode mirror hosted `messageProfile` and profile-local `fallbackLanguage` behavior.
380
382
  * Omitting `offlinePolicy.policyPacks` uses the built-in synthetic opt-in fallback banner. Hosted network fallback uses the same opt-in banner.
381
383
  * `offlinePolicy: { policyPacks: [] }` is explicit no-banner mode.
@@ -64,20 +64,14 @@ Widget copy should be changed through `ConsentManagerProvider.options.i18n` so t
64
64
 
65
65
  ## Advanced: Compound Components
66
66
 
67
- Use compound components only when you need to rearrange the widget's existing primitives:
67
+ Use compound components only when you need to rearrange the widget's existing primitives while keeping policy-aware action grouping:
68
68
 
69
69
  ```tsx
70
70
  <ConsentWidget.Root>
71
71
  <ConsentWidget.Accordion type="multiple">
72
72
  <ConsentWidget.AccordionItems />
73
73
  </ConsentWidget.Accordion>
74
- <ConsentWidget.Footer>
75
- <ConsentWidget.FooterSubGroup>
76
- <ConsentWidget.RejectButton />
77
- <ConsentWidget.AcceptAllButton />
78
- </ConsentWidget.FooterSubGroup>
79
- <ConsentWidget.SaveButton />
80
- </ConsentWidget.Footer>
74
+ <ConsentWidget.PolicyActions />
81
75
  </ConsentWidget.Root>
82
76
  ```
83
77
 
@@ -89,12 +83,71 @@ Use compound components only when you need to rearrange the widget's existing pr
89
83
  * `ConsentWidget.AccordionContent` — Collapsible content area
90
84
  * `ConsentWidget.AccordionArrow` — Expand/collapse indicator
91
85
  * `ConsentWidget.Switch` — Category toggle switch
86
+ * `ConsentWidget.PolicyActions` — Renders grouped policy-aware actions
92
87
  * `ConsentWidget.Footer` — Footer container
93
88
  * `ConsentWidget.FooterSubGroup` — Groups related buttons
94
89
  * `ConsentWidget.AcceptAllButton` — Accepts all consent
95
90
  * `ConsentWidget.RejectButton` — Rejects all consent
96
91
  * `ConsentWidget.SaveButton` — Saves custom selections
97
92
 
93
+ ## Using `renderAction` with c15t Defaults
94
+
95
+ `ConsentWidget.PolicyActions` renders stock c15t buttons and translations by default.
96
+
97
+ ```tsx
98
+ <ConsentWidget.PolicyActions />
99
+ ```
100
+
101
+ `renderAction` is optional. Return the stock button compounds when you want custom mapping while preserving built-in c15t behavior and copy:
102
+
103
+ ```tsx
104
+ <ConsentWidget.PolicyActions
105
+ renderAction={(action, props) => {
106
+ const { key, ...buttonProps } = props
107
+
108
+ switch (action) {
109
+ case 'accept':
110
+ return <ConsentWidget.AcceptAllButton key={key} {...buttonProps} />
111
+ case 'reject':
112
+ return <ConsentWidget.RejectButton key={key} {...buttonProps} />
113
+ case 'customize':
114
+ return <ConsentWidget.SaveButton key={key} {...buttonProps} />
115
+ }
116
+ }}
117
+ />
118
+ ```
119
+
120
+ Use `useTranslations()` only when you are replacing the button markup entirely:
121
+
122
+ ```tsx
123
+ import { ConsentWidget, useTranslations } from '@c15t/react';
124
+
125
+ export function CustomWidgetActions() {
126
+ const { common } = useTranslations();
127
+
128
+ return (
129
+ <ConsentWidget.PolicyActions
130
+ renderAction={(action, props) => (
131
+ <button
132
+ key={props.key}
133
+ type="button"
134
+ className={props.isPrimary ? 'btn-primary' : 'btn-secondary'}
135
+ style={props.style}
136
+ >
137
+ {action === 'accept'
138
+ ? common.acceptAll
139
+ : action === 'reject'
140
+ ? common.rejectAll
141
+ : common.save}
142
+ </button>
143
+ )}
144
+ />
145
+ );
146
+ }
147
+ ```
148
+
149
+ For a fixed footer layout, render `ConsentWidget.Footer` and `ConsentWidget.FooterSubGroup` manually instead of using `ConsentWidget.PolicyActions`.
150
+
98
151
  If the stock widget structure is already correct, stay with tokens and slots instead of rebuilding the layout.
99
152
 
100
153
  ## Props
@@ -4,12 +4,12 @@ description: Choose how c15t connects to its backend - full hosted integration,
4
4
  ---
5
5
  c15t supports three client modes that determine how consent data is stored and synchronized. Choose the mode that matches your infrastructure:
6
6
 
7
- * **Hosted mode** - Full backend integration with geolocation, API sync, and analytics
8
- * **Offline mode** - Local-only storage with no network requests
7
+ * **Hosted mode** - Recommended for production. Backend-backed consent with geolocation, centralized policy resolution, audit history, and offline fallback.
8
+ * **Offline mode** - Browser-only storage with no network requests. Best for local development, demos, static deployments, or controlled fallback scenarios.
9
9
  * **Custom mode** - Bring your own backend with custom endpoint handlers
10
10
 
11
11
  > ℹ️ **Info:**
12
- > Offline mode is browser-only storage. If browser storage is blocked or cleared, consent cannot be remembered and prior choices cannot be verified.
12
+ > If you need durable consent records, server-side enforcement, or automatic jurisdiction detection, use hosted mode. Offline mode cannot provide those guarantees because consent lives only in the browser.
13
13
 
14
14
  <span id="c15t-mode" />
15
15
 
@@ -30,6 +30,13 @@ The default mode. Connects to a c15t backend for full consent lifecycle manageme
30
30
 
31
31
  * `backendURL` (required) - API endpoint path
32
32
 
33
+ **Why it is the default for production:**
34
+
35
+ * The backend stays the source of truth for policy, translations, and jurisdiction logic
36
+ * Consent decisions can be stored beyond the current browser session for audit and support workflows
37
+ * Server-side systems can preload consent-aware behavior instead of waiting for client-only storage
38
+ * If the backend is temporarily unavailable, c15t can fall back locally and re-sync later
39
+
33
40
  **Best for:** Production apps that need geolocation-based jurisdiction detection, consent record storage, and compliance audit trails.
34
41
 
35
42
  ```tsx
@@ -78,10 +85,12 @@ If consent is not stored at all (for example, storage is blocked or frequently c
78
85
 
79
86
  * No automatic geolocation or jurisdiction detection
80
87
  * No consent audit trail
88
+ * No centralized policy or translation updates without shipping frontend changes
81
89
  * No cross-device sync
90
+ * No server-side visibility before client initialization
82
91
  * Works without any backend infrastructure
83
92
 
84
- **Best for:** Static sites, development/testing, or as a starting point before setting up a backend.
93
+ **Best for:** Local development, Storybook/static demos, resilience fallback, or simpler sites that explicitly accept browser-only consent storage.
85
94
 
86
95
  ```tsx
87
96
  import { type ReactNode } from 'react';
@@ -156,7 +165,10 @@ export function ConsentManager({ children }: { children: ReactNode }) {
156
165
  |Feature|Hosted|Offline|Custom|
157
166
  |--|--|--|--|
158
167
  |Geolocation|Automatic|Manual via overrides|Your implementation|
168
+ |Policy source of truth|Backend-managed|Bundled into the frontend|Your implementation|
159
169
  |Consent sync|API|Local only|Your implementation|
170
+ |Audit trail|Backend records|Not available|Your implementation|
171
+ |Server-side consent awareness|Supported|Not available|Your implementation|
160
172
  |SSR data|Supported|Not available|Your implementation|
161
173
  |Analytics|Built-in|Not available|Your implementation|
162
174
  |Infrastructure|c15t backend|None|Your backend|
@@ -88,7 +88,7 @@ If the resolved model is `none` or `opt-out` (and `ui.mode` is `none`), consents
88
88
 
89
89
  ## Debugging the Lifecycle
90
90
 
91
- Use the DevTools panel and callbacks to inspect each step of the initialization flow:
91
+ Use the DevTools panel and callbacks to inspect each step of the initialization flow. `onConsentSet` is the broad lifecycle signal; `onConsentChanged` and `subscribeToConsentChanges()` are the change-only signals for explicit post-init saves.
92
92
 
93
93
  |Step|DevTools Panel|Callback|What to check|
94
94
  |--|--|--|--|
@@ -98,6 +98,7 @@ Use the DevTools panel and callbacks to inspect each step of the initialization
98
98
  |Banner visibility|Consents|—|`activeUI` in store state; does policy `ui.mode` require it?|
99
99
  |Re-prompting|Policy|—|Fingerprint mismatch between stored and resolved policy?|
100
100
  |Consent save|Consents + Events|`onConsentSet`|`preferences` object in callback payload|
101
+ |Change-only integrations|Events|`onConsentChanged` or `subscribeToConsentChanges()`|`allowedCategories`, `deniedCategories`, and previous values only when a real save changed preferences|
101
102
  |Script loading|Scripts|`onConsentSet`|Script IDs and their load/blocked status|
102
103
  |Reload on revocation|Events|`onBeforeConsentRevocationReload`|Fires before reload; check localStorage for `c15t:pending-consent-sync`|
103
104
  |Deferred sync|Events|`onError` (if sync fails)|After reload, check Events panel for successful API call|
@@ -120,7 +121,13 @@ export default function ConsentManager({ children }: { children: ReactNode }) {
120
121
  console.log('Init complete:', { jurisdiction, location });
121
122
  },
122
123
  onConsentSet: ({ preferences }) => {
123
- console.log('Consent saved:', preferences);
124
+ console.log('Broad consent lifecycle event:', preferences);
125
+ },
126
+ onConsentChanged: ({ allowedCategories, deniedCategories }) => {
127
+ console.log('Explicit consent change:', {
128
+ allowedCategories,
129
+ deniedCategories,
130
+ });
124
131
  },
125
132
  onBeforeConsentRevocationReload: ({ preferences }) => {
126
133
  console.log('Reloading due to revocation:', preferences);
@@ -10,9 +10,9 @@ There are three ways to configure policy packs:
10
10
 
11
11
  1. **consent.io (recommended)** — use [consent.io](https://consent.io) as your hosted backend. Configure packs visually in the dashboard or via API — no code changes required. Works with any frontend, including static sites.
12
12
  2. **Self-hosted backend** — define packs in code via `policyPacks` and resolve them from real request geo data. Full control over policy logic and storage.
13
- 3. **Offline fallback** — pass the same policy shapes to the frontend via `offlinePolicy.policyPacks`. Used as a resilience fallback when the backend is unreachable, or for quick local experimentation and demos. If you omit `offlinePolicy.policyPacks`, c15t falls back to a synthetic worldwide opt-in banner instead of no-banner mode.
13
+ 3. **Offline fallback** — pass the same policy shapes to the frontend via `offlinePolicy.policyPacks`. Use this mainly for local development, demos, deterministic testing, or resilience when the backend is temporarily unreachable. If you omit `offlinePolicy.policyPacks`, c15t falls back to a synthetic worldwide opt-in banner instead of no-banner mode.
14
14
 
15
- In both hosted and self-hosted modes, the **backend is always the source of truth**. Offline packs never override a live backend decision.
15
+ In both hosted and self-hosted modes, the **backend is always the source of truth**. Offline packs are a preview or fallback layer and never override a live backend decision.
16
16
 
17
17
  ## Quickstart
18
18