@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.
- package/AGENTS.md +91 -67
- package/README.md +21 -17
- package/SKILL.md +34 -0
- package/dist/aggregate-components.js +1 -1
- package/dist/chunk-warming.js +1 -1
- package/dist/component-hooks/use-headless-consent-ui.js +1 -1
- package/dist/component-hooks/use-headless-iab-consent-ui.js +1 -1
- package/dist/component-hooks/use-panel-trigger.js +1 -1
- package/dist/components/consent-gate/atoms.js +1 -1
- package/dist/components/consent-gate/consent-gate.js +1 -1
- package/dist/components/consent-preferences-link/consent-preferences-link.js +1 -1
- package/dist/components/iab-panel/atoms/card.js +1 -1
- package/dist/components/iab-panel/atoms/feature-item.js +2 -0
- package/dist/components/iab-panel/atoms/header.js +1 -1
- package/dist/components/iab-panel/atoms/purpose-item.js +1 -1
- package/dist/components/iab-panel/atoms/stack-item.js +1 -1
- package/dist/components/iab-panel/iab-panel.js +1 -1
- package/dist/components/iab-panel/index.js +1 -1
- package/dist/components/iab-prompt/atoms/overlay.js +1 -1
- package/dist/components/iab-prompt/atoms/root.js +1 -1
- package/dist/components/panel/atoms/card.js +1 -1
- package/dist/components/panel/atoms/overlay.js +1 -1
- package/dist/components/panel/atoms/root.js +1 -1
- package/dist/components/panel/index.js +1 -1
- package/dist/components/panel/panel.js +1 -1
- package/dist/components/panel-trigger/atoms/button.js +1 -1
- package/dist/components/panel-trigger/atoms/icon.js +1 -1
- package/dist/components/panel-trigger/atoms/root.js +1 -1
- package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
- package/dist/components/panel-trigger/atoms/trigger-icons.js +1 -0
- package/dist/components/panel-trigger/panel-trigger-toolbar.js +1 -1
- package/dist/components/preferences/atoms/accordion.js +1 -1
- package/dist/components/preferences/atoms/root.js +1 -1
- package/dist/components/preferences/atoms/vendor-list.js +1 -1
- package/dist/components/preferences/index.js +1 -1
- package/dist/components/prompt/atoms/overlay.js +1 -1
- package/dist/components/prompt/atoms/root.js +1 -1
- package/dist/components/prompt/components.js +1 -1
- package/dist/components/prompt/policy-actions.js +1 -1
- package/dist/components/shared/libs/compose-refs.js +1 -0
- package/dist/components/shared/libs/slot.js +1 -1
- package/dist/components/shared/primitives/button.js +1 -1
- package/dist/components/shared/ui/accordion/accordion.js +1 -1
- package/dist/components/shared/ui/collapsible/collapsible.js +1 -1
- package/dist/components/shared/ui/dialog/dialog.js +1 -1
- package/dist/components/shared/ui/icon.js +1 -1
- package/dist/components/shared/ui/logo.js +1 -1
- package/dist/components/shared/ui/preference-item/preference-item.js +1 -1
- package/dist/components/shared/ui/switch/switch.js +1 -1
- package/dist/components/shared/ui/tabs/tabs.js +1 -1
- package/dist/consent-theme.js +1 -0
- package/dist/context/consent-tracking-context.js +1 -1
- package/dist/devtools.js +1 -1
- package/dist/draft.js +1 -1
- package/dist/external-iab-context.js +1 -1
- package/dist/headless.js +1 -1
- package/dist/hooks/use-committed-ref.js +2 -0
- package/dist/hooks/use-focus-trap.js +1 -1
- package/dist/hooks.js +1 -1
- package/dist/iab/styles.tw3.css +5 -45
- package/dist/iab-context.js +1 -1
- package/dist/index.js +1 -1
- package/dist/kernel-selector.js +1 -0
- package/dist/module-hooks/iframe-blocker.js +1 -1
- package/dist/module-hooks/network-blocker.js +1 -1
- package/dist/module-hooks/network-hold.js +2 -0
- package/dist/module-hooks/persistence.js +1 -1
- package/dist/panel.js +1 -1
- package/dist/postcss-tailwind3.js +1 -0
- package/dist/preferences.js +1 -2
- package/dist/provider.js +1 -1
- package/dist/server/fetch-ssr-data.js +1 -1
- package/dist/server/headers.js +1 -1
- package/dist/server/normalize-url.js +1 -1
- package/dist/styles.tw3.css +16 -61
- package/dist/transports/offline.js +1 -1
- package/dist/ui-save.js +1 -1
- package/dist/utils/deferred-module.js +1 -1
- package/dist/utils/merge-slot-props.js +1 -1
- package/dist/utils/theme-utils.js +1 -1
- package/dist/version.js +1 -1
- package/dist-types/chunk-warming.d.ts +41 -8
- package/dist-types/component-hooks/use-headless-consent-ui.d.ts +1 -1
- package/dist-types/component-hooks/use-headless-iab-consent-ui.d.ts +1 -1
- package/dist-types/components/iab-panel/atoms/feature-item.d.ts +28 -0
- package/dist-types/components/iab-panel/atoms/purpose-item.d.ts +4 -1
- package/dist-types/components/iab-panel/atoms/stack-item.d.ts +6 -1
- package/dist-types/components/iab-panel/iab-panel.d.ts +2 -2
- package/dist-types/components/iab-panel/index.d.ts +6 -2
- package/dist-types/components/iab-panel/use-iab-translations.d.ts +4 -0
- package/dist-types/components/iab-prompt/iab-prompt.d.ts +2 -2
- package/dist-types/components/iab-prompt/index.d.ts +2 -2
- package/dist-types/components/panel/atoms/card.d.ts +1 -0
- package/dist-types/components/panel/index.d.ts +1 -0
- package/dist-types/components/panel/panel.d.ts +1 -1
- package/dist-types/components/panel-trigger/atoms/trigger-icons.d.ts +20 -0
- package/dist-types/components/preferences/index.d.ts +1 -0
- package/dist-types/components/prompt/components.d.ts +6 -0
- package/dist-types/components/shared/libs/compose-refs.d.ts +20 -0
- package/dist-types/components/shared/ui/accordion/accordion.d.ts +1 -0
- package/dist-types/components/shared/ui/collapsible/collapsible.d.ts +1 -0
- package/dist-types/components/shared/ui/icon.d.ts +2 -1
- package/dist-types/components/shared/ui/logo.d.ts +0 -10
- package/dist-types/components/shared/ui/preference-item/preference-item.d.ts +14 -0
- package/dist-types/components/shared/ui/switch/switch.d.ts +1 -0
- package/dist-types/components/shared/ui/tabs/tabs.d.ts +1 -0
- package/dist-types/consent-theme.d.ts +38 -0
- package/dist-types/context/consent-tracking-context.d.ts +10 -0
- package/dist-types/context/theme-context.d.ts +2 -2
- package/dist-types/context.d.ts +6 -0
- package/dist-types/devtools.d.ts +13 -2
- package/dist-types/draft.d.ts +50 -7
- package/dist-types/headless.d.ts +0 -1
- package/dist-types/hooks/use-committed-ref.d.ts +24 -0
- package/dist-types/hooks/use-focus-trap.d.ts +3 -1
- package/dist-types/hooks.d.ts +56 -13
- package/dist-types/iab.d.ts +1 -1
- package/dist-types/index.d.ts +4 -3
- package/dist-types/kernel-selector.d.ts +66 -0
- package/dist-types/module-hooks/iframe-blocker.d.ts +27 -0
- package/dist-types/module-hooks/network-blocker.d.ts +23 -0
- package/dist-types/module-hooks/network-hold.d.ts +47 -0
- package/dist-types/panel.d.ts +13 -1
- package/dist-types/postcss-tailwind3.d.ts +21 -0
- package/dist-types/preferences.d.ts +12 -1
- package/dist-types/provider.d.ts +62 -12
- package/dist-types/server/headers.d.ts +14 -4
- package/dist-types/server/normalize-url.d.ts +14 -7
- package/dist-types/server/types.d.ts +12 -1
- package/dist-types/transports/offline.d.ts +7 -16
- package/dist-types/ui-config-context.d.ts +9 -1
- package/dist-types/ui-save.d.ts +21 -1
- package/dist-types/utils/deferred-module.d.ts +3 -1
- package/dist-types/utils/merge-slot-props.d.ts +15 -1
- package/dist-types/utils/theme-utils.d.ts +17 -2
- package/dist-types/version.d.ts +1 -1
- package/docs/README.md +91 -67
- package/docs/assets/v3/bottom-bar.png +0 -0
- package/docs/assets/v3/brand-card.png +0 -0
- package/docs/assets/v3/brand-preferences.png +0 -0
- package/docs/assets/v3/choice-wall.png +0 -0
- package/docs/assets/v3/headless-bar-html.png +0 -0
- package/docs/assets/v3/headless-bar-mobile.png +0 -0
- package/docs/assets/v3/headless-bar.png +0 -0
- package/docs/assets/v3/slim-bar.png +0 -0
- package/docs/concepts/choose-your-setup.md +87 -0
- package/docs/{frameworks/react/concepts → concepts}/consent-categories.md +30 -8
- package/docs/concepts/consent-state.md +357 -0
- package/docs/{guides → concepts}/data-fetching.md +31 -27
- package/docs/concepts/how-consent-works.md +123 -0
- package/docs/{frameworks/react/concepts/policy-presets.md → concepts/policies.md} +7 -59
- package/docs/customization/class-names.md +202 -0
- package/docs/customization/dark-mode.md +157 -0
- package/docs/customization/motion.md +119 -0
- package/docs/customization/overview.md +67 -33
- package/docs/customization/recipes.md +839 -47
- package/docs/customization/slots.md +216 -35
- package/docs/customization/stylesheets.md +147 -0
- package/docs/customization/tailwind.md +842 -0
- package/docs/customization/tokens.md +166 -36
- package/docs/customization/translations.md +61 -3
- package/docs/frameworks/react/callbacks.md +118 -0
- package/docs/frameworks/react/clear-on-revocation.md +106 -0
- package/docs/frameworks/react/components/consent-banner.md +42 -28
- package/docs/frameworks/react/components/consent-dialog-link.md +13 -11
- package/docs/frameworks/react/components/consent-dialog-trigger.md +81 -76
- package/docs/frameworks/react/components/consent-dialog.md +100 -55
- package/docs/frameworks/react/components/consent-gate.md +45 -24
- package/docs/frameworks/react/components/consent-provider.md +126 -0
- package/docs/frameworks/react/components/consent-widget.md +32 -20
- package/docs/frameworks/react/components/dev-tools.md +52 -84
- package/docs/frameworks/react/components.md +105 -0
- package/docs/frameworks/react/compose.md +354 -0
- package/docs/frameworks/react/content-security-policy.md +126 -0
- package/docs/frameworks/react/customize.md +250 -0
- package/docs/frameworks/react/embeds.md +89 -0
- package/docs/frameworks/react/hooks.md +65 -0
- package/docs/frameworks/react/iab.md +222 -0
- package/docs/frameworks/react/network-blocker.md +140 -0
- package/docs/frameworks/react/quickstart.md +139 -101
- package/docs/frameworks/react/rendering.md +128 -0
- package/docs/frameworks/react/scripts.md +115 -0
- package/docs/frameworks/react/translations.md +155 -0
- package/docs/frameworks/react/troubleshooting.md +92 -47
- package/docs/frameworks/react/vendor-consent.md +170 -0
- package/docs/guides/banner-experiments.md +654 -0
- package/docs/guides/troubleshooting.md +120 -47
- package/docs/guides/verify-consent.md +81 -49
- package/docs/integrations/adobe-analytics.md +168 -160
- package/docs/integrations/ahrefs-analytics.md +153 -155
- package/docs/integrations/amplitude.md +163 -157
- package/docs/integrations/building-integrations.md +136 -37
- package/docs/integrations/clearbit.md +155 -155
- package/docs/integrations/cloudflare-web-analytics.md +157 -157
- package/docs/integrations/cloudflare-zaraz.md +210 -262
- package/docs/integrations/crisp.md +165 -159
- package/docs/integrations/databuddy.md +158 -174
- package/docs/integrations/fathom-analytics.md +159 -157
- package/docs/integrations/front-chat.md +322 -0
- package/docs/integrations/google-maps.md +119 -84
- package/docs/integrations/google-tag-manager.md +179 -164
- package/docs/integrations/google-tag.md +164 -161
- package/docs/integrations/heap.md +164 -156
- package/docs/integrations/hightouch.md +162 -158
- package/docs/integrations/hotjar.md +160 -156
- package/docs/integrations/intercom.md +184 -154
- package/docs/integrations/klaviyo.md +486 -0
- package/docs/integrations/linkedin-insights.md +175 -151
- package/docs/integrations/logrocket.md +161 -157
- package/docs/integrations/matomo-analytics.md +189 -179
- package/docs/integrations/meta-pixel.md +189 -151
- package/docs/integrations/microsoft-clarity.md +164 -156
- package/docs/integrations/microsoft-uet.md +149 -155
- package/docs/integrations/mixpanel-analytics.md +156 -161
- package/docs/integrations/one-dollar-stats.md +306 -0
- package/docs/integrations/openai-pixel.md +205 -302
- package/docs/integrations/overview.md +143 -83
- package/docs/integrations/pinterest-tag.md +329 -0
- package/docs/integrations/pirsch.md +170 -160
- package/docs/integrations/plausible-analytics.md +173 -159
- package/docs/integrations/posthog.md +227 -242
- package/docs/integrations/promptwatch.md +155 -155
- package/docs/integrations/reddit-pixel.md +186 -158
- package/docs/integrations/rudderstack.md +202 -187
- package/docs/integrations/rybbit-analytics.md +172 -161
- package/docs/integrations/segment.md +183 -155
- package/docs/integrations/snapchat-pixel.md +187 -157
- package/docs/integrations/tiktok-pixel.md +172 -151
- package/docs/integrations/umami-analytics.md +164 -159
- package/docs/integrations/vercel-analytics.md +167 -158
- package/docs/integrations/x-pixel.md +177 -151
- package/docs/integrations/youtube.md +122 -87
- package/docs/upgrade-v3.md +490 -354
- package/package.json +15 -9
- package/dist/component-hooks/use-manager.js +0 -2
- package/dist-types/component-hooks/use-manager.d.ts +0 -45
- package/docs/assets/v3/brand-bar.png +0 -0
- package/docs/assets/v3/mobile-card.png +0 -0
- package/docs/assets/v3/preferences.png +0 -0
- package/docs/frameworks/react/components/consent-manager-provider.md +0 -75
- package/docs/frameworks/react/hooks/use-consent-manager/overview.md +0 -58
- package/docs/frameworks/react/iab/overview.md +0 -53
- package/docs/frameworks/react/script-loader.md +0 -63
- package/docs/frameworks/react/styling/overview.md +0 -217
- package/docs/guides/consent-state.md +0 -60
- package/docs/guides/deployment-modes.md +0 -75
- package/docs/integrations/clear-on-revocation.md +0 -167
- 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.
|