@c15t/nextjs 2.0.0-rc.6 → 2.0.0-rc.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/iab/styles.css +12 -1
- package/dist/iab/styles.tw3.css +14 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/styles.css +10 -1
- package/dist/styles.tw3.css +13 -0
- package/dist/version.cjs +1 -1
- package/dist/version.js +1 -1
- package/dist-types/headless.d.ts +1 -1
- package/dist-types/index.d.ts +2 -2
- package/dist-types/libs/browser-initial-data.d.ts +2 -2
- package/dist-types/libs/initial-data.d.ts +1 -1
- package/dist-types/types.d.ts +3 -3
- package/dist-types/version.d.ts +1 -1
- package/docs/building-headless-components.md +43 -22
- package/docs/callbacks.md +76 -9
- package/docs/components/consent-banner.md +148 -24
- package/docs/components/consent-dialog.md +42 -3
- package/docs/components/consent-manager-provider.md +3 -1
- package/docs/components/consent-widget.md +91 -9
- package/docs/concepts/client-modes.md +16 -4
- package/docs/concepts/initialization-flow.md +9 -2
- package/docs/concepts/policy-packs.md +2 -2
- package/docs/headless.md +13 -7
- package/docs/hooks/use-consent-manager/overview.md +17 -3
- package/docs/hooks/use-ssr-status.md +1 -1
- package/docs/hooks/use-translations.md +1 -0
- package/docs/iab/consent-banner.md +2 -5
- package/docs/iab/consent-dialog.md +3 -6
- package/docs/iab/overview.md +11 -5
- package/docs/integrations/building-integrations.md +405 -0
- package/docs/integrations/databuddy.md +22 -5
- package/docs/integrations/google-tag-manager.md +2 -2
- package/docs/integrations/google-tag.md +2 -29
- package/docs/integrations/linkedin-insights.md +1 -1
- package/docs/integrations/meta-pixel.md +1 -1
- package/docs/integrations/microsoft-uet.md +1 -1
- package/docs/integrations/overview.md +18 -2
- package/docs/integrations/posthog.md +39 -17
- package/docs/integrations/tiktok-pixel.md +1 -1
- package/docs/integrations/x-pixel.md +1 -1
- package/docs/optimization.md +68 -9
- package/docs/policy-packs.md +7 -7
- package/docs/quickstart.md +11 -5
- package/docs/script-loader.md +22 -1
- package/docs/server-side.md +1 -1
- package/docs/styling/classnames.md +17 -9
- package/docs/styling/overview.md +166 -29
- package/docs/styling/slots.md +37 -7
- package/docs/styling/tailwind.md +25 -27
- package/iab/styles.css +1 -0
- package/package.json +15 -7
- package/readme.json +4 -0
- package/src/iab/styles.css +12 -0
- package/src/iab/styles.tw3.css +14 -0
- package/src/styles.css +10 -0
- package/src/styles.tw3.css +13 -0
- package/styles.css +1 -0
package/README.md
CHANGED
|
@@ -63,6 +63,13 @@ The CLI will:
|
|
|
63
63
|
pnpm add @c15t/nextjs
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
Then add the prebuilt stylesheet to your app-level CSS entrypoint:
|
|
67
|
+
|
|
68
|
+
```css
|
|
69
|
+
/* app/globals.css */
|
|
70
|
+
@import "@c15t/nextjs/styles.css";
|
|
71
|
+
```
|
|
72
|
+
|
|
66
73
|
To manually install, follow the guide in our [docs – manual setup](https://c15t.com/docs/frameworks/nextjs/quickstart#manual-setup).
|
|
67
74
|
|
|
68
75
|
## Usage
|
package/dist/iab/styles.css
CHANGED
|
@@ -1 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @c15t/nextjs — IAB TCF component styles.
|
|
3
|
+
*
|
|
4
|
+
* Add this stylesheet to the same global CSS entrypoint as your base c15t styles
|
|
5
|
+
* when using IAB consent components in Next.js. Do not import either stylesheet
|
|
6
|
+
* from JS/TSX.
|
|
7
|
+
*
|
|
8
|
+
* Usage (app/globals.css):
|
|
9
|
+
* @import "@c15t/nextjs/styles.css";
|
|
10
|
+
* @import "@c15t/nextjs/iab/styles.css";
|
|
11
|
+
*/
|
|
12
|
+
@import "@c15t/react/iab/styles.css";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @c15t/nextjs/iab — Tailwind 3-compatible IAB component styles.
|
|
3
|
+
*
|
|
4
|
+
* Add this stylesheet to the same global CSS entrypoint as your base c15t styles.
|
|
5
|
+
* Do not import either stylesheet from JS/TSX files.
|
|
6
|
+
*
|
|
7
|
+
* Usage (app/globals.css):
|
|
8
|
+
* @tailwind base;
|
|
9
|
+
* @tailwind components;
|
|
10
|
+
* @import "@c15t/nextjs/styles.tw3.css";
|
|
11
|
+
* @import "@c15t/nextjs/iab/styles.tw3.css";
|
|
12
|
+
* @tailwind utilities;
|
|
13
|
+
*/
|
|
14
|
+
@import "@c15t/react/iab/styles.tw3.css";
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const __rslib_import_meta_url__="u"<typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_modules__={"./libs/browser-initial-data"(e){e.exports=require("./libs/browser-initial-data.cjs")},"./libs/initial-data"(e){e.exports=require("./libs/initial-data.cjs")},"@c15t/react"(e){e.exports=require("@c15t/react")},c15t(e){e.exports=require("c15t")}},__webpack_module_cache__={};function __webpack_require__(e){var _=__webpack_module_cache__[e];if(void 0!==_)return _.exports;var
|
|
1
|
+
"use strict";const __rslib_import_meta_url__="u"<typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_modules__={"./libs/browser-initial-data"(e){e.exports=require("./libs/browser-initial-data.cjs")},"./libs/initial-data"(e){e.exports=require("./libs/initial-data.cjs")},"@c15t/react"(e){e.exports=require("@c15t/react")},c15t(e){e.exports=require("c15t")}},__webpack_module_cache__={};function __webpack_require__(e){var _=__webpack_module_cache__[e];if(void 0!==_)return _.exports;var r=__webpack_module_cache__[e]={exports:{}};return __webpack_modules__[e](r,r.exports,__webpack_require__),r.exports}__webpack_require__.n=e=>{var _=e&&e.__esModule?()=>e.default:()=>e;return __webpack_require__.d(_,{a:_}),_},__webpack_require__.d=(e,_)=>{for(var r in _)__webpack_require__.o(_,r)&&!__webpack_require__.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:_[r]})},__webpack_require__.o=(e,_)=>Object.prototype.hasOwnProperty.call(e,_),__webpack_require__.r=e=>{"u">typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};for(var __rspack_i in(()=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{C15tPrefetch:()=>t.C15tPrefetch,buildPrefetchScript:()=>r.buildPrefetchScript,fetchInitialData:()=>a.fetchInitialData});var e=__webpack_require__("@c15t/react"),_={};for(let r in e)0>["fetchInitialData","default","buildPrefetchScript","C15tPrefetch"].indexOf(r)&&(_[r]=()=>e[r]);__webpack_require__.d(__webpack_exports__,_);var r=__webpack_require__("c15t"),t=__webpack_require__("./libs/browser-initial-data"),a=__webpack_require__("./libs/initial-data")})(),exports.C15tPrefetch=__webpack_exports__.C15tPrefetch,exports.buildPrefetchScript=__webpack_exports__.buildPrefetchScript,exports.fetchInitialData=__webpack_exports__.fetchInitialData,__webpack_exports__)-1===["C15tPrefetch","buildPrefetchScript","fetchInitialData"].indexOf(__rspack_i)&&(exports[__rspack_i]=__webpack_exports__[__rspack_i]);Object.defineProperty(exports,"__esModule",{value:!0});
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"@c15t/react";export{buildPrefetchScript
|
|
1
|
+
export*from"@c15t/react";export{buildPrefetchScript}from"c15t";export{C15tPrefetch}from"./libs/browser-initial-data.js";export{fetchInitialData}from"./libs/initial-data.js";
|
package/dist/styles.css
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @c15t/nextjs — Non-IAB prebuilt component styles.
|
|
3
|
+
*
|
|
4
|
+
* Import this stylesheet once from your app-level CSS entrypoint when using
|
|
5
|
+
* prebuilt (styled) consent components.
|
|
6
|
+
*
|
|
7
|
+
* Usage (app/globals.css):
|
|
8
|
+
* @import "@c15t/nextjs/styles.css";
|
|
9
|
+
*/
|
|
10
|
+
@import "@c15t/react/styles.css";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @c15t/nextjs — Tailwind 3-compatible prebuilt component styles.
|
|
3
|
+
*
|
|
4
|
+
* Import this stylesheet in the same global CSS entrypoint as Tailwind 3,
|
|
5
|
+
* after `@tailwind components;` and before `@tailwind utilities;`.
|
|
6
|
+
*
|
|
7
|
+
* Usage (app/globals.css):
|
|
8
|
+
* @tailwind base;
|
|
9
|
+
* @tailwind components;
|
|
10
|
+
* @import "@c15t/nextjs/styles.tw3.css";
|
|
11
|
+
* @tailwind utilities;
|
|
12
|
+
*/
|
|
13
|
+
@import "@c15t/react/styles.tw3.css";
|
package/dist/version.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const __rslib_import_meta_url__="u"<typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_require__={};__webpack_require__.d=(e,_)=>{for(var r in _)__webpack_require__.o(_,r)&&!__webpack_require__.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:_[r]})},__webpack_require__.o=(e,_)=>Object.prototype.hasOwnProperty.call(e,_),__webpack_require__.r=e=>{"u">typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{version:()=>version});const version="2.0.0-rc.
|
|
1
|
+
"use strict";const __rslib_import_meta_url__="u"<typeof document?new(require("url".replace("",""))).URL("file:"+__filename).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href;var __webpack_require__={};__webpack_require__.d=(e,_)=>{for(var r in _)__webpack_require__.o(_,r)&&!__webpack_require__.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:_[r]})},__webpack_require__.o=(e,_)=>Object.prototype.hasOwnProperty.call(e,_),__webpack_require__.r=e=>{"u">typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{version:()=>version});const version="2.0.0-rc.8";for(var __rspack_i in exports.version=__webpack_exports__.version,__webpack_exports__)-1===["version"].indexOf(__rspack_i)&&(exports[__rspack_i]=__webpack_exports__[__rspack_i]);Object.defineProperty(exports,"__esModule",{value:!0});
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
let e="2.0.0-rc.
|
|
1
|
+
let e="2.0.0-rc.8";export{e as version};
|
package/dist-types/headless.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from '
|
|
1
|
+
export * from '@c15t/react/headless';
|
package/dist-types/index.d.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* @see {@link @c15t/react} for React components and hooks
|
|
8
8
|
* @see {@link ./middleware} for Next.js middleware integration
|
|
9
9
|
*/
|
|
10
|
-
export * from '
|
|
11
|
-
export { buildPrefetchScript,
|
|
10
|
+
export * from '@c15t/react';
|
|
11
|
+
export { buildPrefetchScript, type PrefetchOptions } from 'c15t';
|
|
12
12
|
export { C15tPrefetch } from './libs/browser-initial-data';
|
|
13
13
|
export { fetchInitialData } from './libs/initial-data';
|
|
14
14
|
export type { C15tPrefetchProps, ConsentManagerProps, FetchInitialDataOptions, InitialDataPromise, } from './types';
|
|
@@ -3,7 +3,7 @@ import type { C15tPrefetchProps } from '../types';
|
|
|
3
3
|
* Next.js script component that starts `/init` prefetching before hydration.
|
|
4
4
|
*
|
|
5
5
|
* @remarks
|
|
6
|
-
* Use in `app/layout.tsx` for static routes.
|
|
7
|
-
*
|
|
6
|
+
* Use in `app/layout.tsx` for static routes. Matching prefetched data is
|
|
7
|
+
* consumed automatically by the runtime during first store initialization.
|
|
8
8
|
*/
|
|
9
9
|
export declare function C15tPrefetch({ id, ...options }: C15tPrefetchProps): import("react/jsx-runtime").JSX.Element;
|
package/dist-types/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ConsentManagerProviderProps } from '
|
|
2
|
-
import type { FetchSSRDataOptionsBase } from '
|
|
3
|
-
import type { PrefetchOptions } from '
|
|
1
|
+
import type { ConsentManagerProviderProps } from '@c15t/react';
|
|
2
|
+
import type { FetchSSRDataOptionsBase } from '@c15t/react/server';
|
|
3
|
+
import type { PrefetchOptions } from 'c15t';
|
|
4
4
|
export type InitialDataPromise = NonNullable<ConsentManagerProviderProps['options']['store']>['ssrData'];
|
|
5
5
|
export interface NextCacheOptions {
|
|
6
6
|
/**
|
package/dist-types/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "2.0.0-rc.
|
|
1
|
+
export declare const version = "2.0.0-rc.8";
|
|
@@ -2,15 +2,27 @@
|
|
|
2
2
|
title: Building Headless Components
|
|
3
3
|
description: Build policy-aware custom consent components in Next.js using the headless hooks and policy-pack tooling.
|
|
4
4
|
---
|
|
5
|
-
Building
|
|
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
6
|
|
|
7
|
-
The
|
|
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
|
|
12
|
+
|
|
13
|
+
> ⚠️ **Warning:**
|
|
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.
|
|
15
|
+
|
|
16
|
+
The headless stack underneath that is:
|
|
8
17
|
|
|
9
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
|
|
10
20
|
* `useConsentManager()` for runtime state, categories, selected consent state, and policy metadata
|
|
11
21
|
* `useTranslations()` for the resolved copy
|
|
12
22
|
* `offlinePolicy.policyPacks` for offline previews that behave like backend policy resolution
|
|
13
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
|
+
|
|
14
26
|
> ℹ️ **Info:**
|
|
15
27
|
> This guide is about building your own components while still respecting resolved policy-pack behavior. For the general headless overview, see Headless Mode.
|
|
16
28
|
|
|
@@ -30,6 +42,11 @@ The main win is that your custom UI can stay aligned with policy packs without d
|
|
|
30
42
|
|
|
31
43
|
That means your component mostly focuses on markup and design-system concerns instead of re-implementing policy interpretation.
|
|
32
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
|
+
|
|
33
50
|
## Provider Setup for Local Policy Testing
|
|
34
51
|
|
|
35
52
|
```tsx title="components/consent-manager/provider.tsx"
|
|
@@ -170,25 +187,29 @@ export function CustomConsentDialog() {
|
|
|
170
187
|
))}
|
|
171
188
|
</div>
|
|
172
189
|
|
|
173
|
-
<div className="mt-4
|
|
174
|
-
{dialog.
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
<div className="mt-4 space-y-2">
|
|
191
|
+
{dialog.actionGroups.map((group, index) => (
|
|
192
|
+
<div key={`${group.join('-')}-${index}`} className="flex gap-2">
|
|
193
|
+
{group.map((action) => (
|
|
194
|
+
<button
|
|
195
|
+
key={action}
|
|
196
|
+
type="button"
|
|
197
|
+
onClick={() => {
|
|
198
|
+
if (action === 'customize') {
|
|
199
|
+
void saveCustomPreferences();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
void performDialogAction(action);
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
{action === 'accept'
|
|
206
|
+
? translations.common.acceptAll
|
|
207
|
+
: action === 'reject'
|
|
208
|
+
? translations.common.rejectAll
|
|
209
|
+
: translations.common.save}
|
|
210
|
+
</button>
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
192
213
|
))}
|
|
193
214
|
</div>
|
|
194
215
|
</section>
|
|
@@ -201,7 +222,7 @@ export function CustomConsentDialog() {
|
|
|
201
222
|
When you build custom banner or dialog components, make sure they use:
|
|
202
223
|
|
|
203
224
|
* `activeUI` or `banner.isVisible` / `dialog.isVisible` for visibility
|
|
204
|
-
* `allowedActions`, `
|
|
225
|
+
* `allowedActions`, `actionGroups`, and `primaryActions` instead of hard-coding buttons
|
|
205
226
|
* `primaryActions` for visual emphasis
|
|
206
227
|
* `consentCategories` when deciding which category toggles to render
|
|
207
228
|
* `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
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
##
|
|
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/nextjs';
|
|
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
|
-
|
|
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`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: ConsentBanner
|
|
3
|
-
description: A pre-built consent banner that appears when user consent is needed. Supports
|
|
3
|
+
description: A pre-built consent banner that appears when user consent is needed. Supports policy-aware layout, theming, and advanced composition when markup must change.
|
|
4
4
|
---
|
|
5
5
|
`ConsentBanner` is a ready-to-use consent banner that appears automatically in **opt-in jurisdictions** (like GDPR) where explicit consent is required before tracking. In opt-out jurisdictions (like CCPA), the banner won't appear — users get an opt-out mechanism instead. It includes reject, accept, and customize buttons with configurable layout.
|
|
6
6
|
|
|
@@ -18,20 +18,6 @@ export function ConsentManager() {
|
|
|
18
18
|
}
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
## Customizing Content
|
|
22
|
-
|
|
23
|
-
Override the default title, description, and button labels:
|
|
24
|
-
|
|
25
|
-
```tsx
|
|
26
|
-
<ConsentBanner
|
|
27
|
-
title="We value your privacy"
|
|
28
|
-
description="We use cookies to enhance your browsing experience and analyze site traffic."
|
|
29
|
-
acceptButtonText="Accept all"
|
|
30
|
-
rejectButtonText="Reject all"
|
|
31
|
-
customizeButtonText="Manage preferences"
|
|
32
|
-
/>
|
|
33
|
-
```
|
|
34
|
-
|
|
35
21
|
## Button Layout
|
|
36
22
|
|
|
37
23
|
The `layout` prop controls button arrangement. Each item is either a button ID or an array of button IDs (which groups them together):
|
|
@@ -86,6 +72,37 @@ Use the provider `theme` prop to control how stock consent actions look:
|
|
|
86
72
|
|
|
87
73
|
Policy packs control grouping, ordering, and direction. The theme controls button appearance.
|
|
88
74
|
|
|
75
|
+
## Styling First
|
|
76
|
+
|
|
77
|
+
> ℹ️ **Info:**
|
|
78
|
+
> For pure theming, stay inside the pre-built banner. Start with layout props, theme.consentActions, design tokens, and theme.slots before reaching for compound components. See Styling Overview.
|
|
79
|
+
|
|
80
|
+
The stock banner maps common visual changes to the theme system:
|
|
81
|
+
|
|
82
|
+
* Card background -> `theme.colors.surface`
|
|
83
|
+
* Footer background -> `theme.colors.surfaceHover`
|
|
84
|
+
* Card, footer, and title tweaks -> `theme.slots.consentBannerCard`, `consentBannerFooter`, and `consentBannerTitle`
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<ConsentManagerProvider
|
|
88
|
+
options={{
|
|
89
|
+
theme: {
|
|
90
|
+
colors: {
|
|
91
|
+
surface: '#fffdf8',
|
|
92
|
+
surfaceHover: '#f6f3ee',
|
|
93
|
+
},
|
|
94
|
+
slots: {
|
|
95
|
+
consentBannerCard: 'rounded-[28px] shadow-xl',
|
|
96
|
+
consentBannerFooter: 'border-t border-black/10 px-6',
|
|
97
|
+
consentBannerTitle: 'tracking-tight',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<ConsentBanner />
|
|
103
|
+
</ConsentManagerProvider>
|
|
104
|
+
```
|
|
105
|
+
|
|
89
106
|
### Primary Button
|
|
90
107
|
|
|
91
108
|
Highlight specific button(s) as the primary action:
|
|
@@ -116,9 +133,40 @@ Control which legal links appear in the banner description:
|
|
|
116
133
|
> ℹ️ **Info:**
|
|
117
134
|
> Legal link URLs are configured in the ConsentManagerProvider options via the legalLinks prop, not on the banner itself.
|
|
118
135
|
|
|
119
|
-
##
|
|
136
|
+
## Customizing Copy
|
|
120
137
|
|
|
121
|
-
|
|
138
|
+
Prefer provider `i18n` when you want to rename the stock banner content:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<ConsentManagerProvider
|
|
142
|
+
options={{
|
|
143
|
+
i18n: {
|
|
144
|
+
locale: 'en',
|
|
145
|
+
messages: {
|
|
146
|
+
en: {
|
|
147
|
+
cookieBanner: {
|
|
148
|
+
title: 'We value your privacy',
|
|
149
|
+
description: 'We use cookies to improve the site and measure performance.',
|
|
150
|
+
},
|
|
151
|
+
common: {
|
|
152
|
+
acceptAll: 'Accept all',
|
|
153
|
+
rejectAll: 'Reject all',
|
|
154
|
+
customize: 'Manage preferences',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
<ConsentBanner />
|
|
162
|
+
</ConsentManagerProvider>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Direct text props such as `title`, `description`, and `acceptButtonText` are still supported for one-off overrides, but `i18n` is the preferred path for copy changes.
|
|
166
|
+
|
|
167
|
+
## Advanced: Compound Components
|
|
168
|
+
|
|
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:
|
|
122
170
|
|
|
123
171
|
```tsx
|
|
124
172
|
<ConsentBanner.Root>
|
|
@@ -128,13 +176,7 @@ Build fully custom banner layouts using sub-components:
|
|
|
128
176
|
<ConsentBanner.Title />
|
|
129
177
|
<ConsentBanner.Description />
|
|
130
178
|
</ConsentBanner.Header>
|
|
131
|
-
<ConsentBanner.
|
|
132
|
-
<ConsentBanner.FooterSubGroup>
|
|
133
|
-
<ConsentBanner.RejectButton />
|
|
134
|
-
<ConsentBanner.AcceptButton />
|
|
135
|
-
</ConsentBanner.FooterSubGroup>
|
|
136
|
-
<ConsentBanner.CustomizeButton />
|
|
137
|
-
</ConsentBanner.Footer>
|
|
179
|
+
<ConsentBanner.PolicyActions />
|
|
138
180
|
</ConsentBanner.Card>
|
|
139
181
|
</ConsentBanner.Root>
|
|
140
182
|
```
|
|
@@ -144,6 +186,7 @@ Build fully custom banner layouts using sub-components:
|
|
|
144
186
|
* `ConsentBanner.Header` — Contains title and description
|
|
145
187
|
* `ConsentBanner.Title` — Heading, defaults to translation `consentBanner.title`
|
|
146
188
|
* `ConsentBanner.Description` — Description text, supports `legalLinks` prop
|
|
189
|
+
* `ConsentBanner.PolicyActions` — Renders policy-aware grouped actions inside the banner footer
|
|
147
190
|
* `ConsentBanner.Footer` — Action buttons container
|
|
148
191
|
* `ConsentBanner.FooterSubGroup` — Groups related buttons together
|
|
149
192
|
* `ConsentBanner.RejectButton` — Rejects all consent
|
|
@@ -151,6 +194,86 @@ Build fully custom banner layouts using sub-components:
|
|
|
151
194
|
* `ConsentBanner.AcceptButton` — Accepts all consent
|
|
152
195
|
* `ConsentBanner.Overlay` — Optional backdrop overlay
|
|
153
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
|
+
|
|
275
|
+
If you only need styling changes, stay with tokens and slots instead of rebuilding the banner layout.
|
|
276
|
+
|
|
154
277
|
## Props
|
|
155
278
|
|
|
156
279
|
### ConsentBannerProps
|
|
@@ -167,6 +290,7 @@ Build fully custom banner layouts using sub-components:
|
|
|
167
290
|
|trapFocus|boolean \|undefined|When true, the consent banner will trap focus|true|Optional|
|
|
168
291
|
|disableAnimation|boolean \|undefined|When true, disables the entrance/exit animations|false|Optional|
|
|
169
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|
|
|
170
294
|
|layout|ConsentBannerLayout \|undefined|Defines the layout of buttons in the footer. Allows reordering and grouping of buttons.|-|Optional|
|
|
171
295
|
|direction|PolicyUiActionDirection \|undefined|Defines how footer button groups flow.|-|Optional|
|
|
172
296
|
|primaryButton|ConsentBannerButton \|undefined|Specifies which button(s) should be highlighted as the primary action.|-|Optional|
|