@c15t/scripts 3.0.0-alpha.1 → 3.0.0-alpha.3
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 +129 -59
- package/README.md +8 -29
- package/SKILL.md +33 -0
- package/dist/adobe-analytics.js +2 -0
- package/dist/ahrefs-analytics.js +2 -0
- package/dist/amplitude.js +2 -0
- package/dist/clearbit.js +2 -0
- package/dist/cloudflare-web-analytics.js +2 -0
- package/dist/cloudflare-zaraz.js +2 -0
- package/dist/crisp.js +2 -0
- package/dist/databuddy.js +2 -0
- package/dist/e2e-test-utils.js +2 -137
- package/dist/engine/compile.js +2 -89
- package/dist/engine/runtime.js +2 -448
- package/dist/events.js +2 -0
- package/dist/fathom-analytics.js +2 -0
- package/dist/front-chat.js +2 -0
- package/dist/google-tag-manager.js +2 -0
- package/dist/google-tag.js +2 -0
- package/dist/heap.js +2 -0
- package/dist/hightouch.js +2 -0
- package/dist/hotjar.js +2 -0
- package/dist/intercom.js +2 -0
- package/dist/klaviyo.js +2 -0
- package/dist/linkedin-insights.js +2 -0
- package/dist/logrocket.js +2 -0
- package/dist/matomo-analytics.js +2 -0
- package/dist/meta-pixel.js +2 -0
- package/dist/microsoft-clarity.js +2 -0
- package/dist/microsoft-uet.js +2 -0
- package/dist/mixpanel-analytics.js +2 -0
- package/dist/one-dollar-stats.js +2 -0
- package/dist/openai-pixel.js +2 -0
- package/dist/pinterest-tag.js +2 -0
- package/dist/pirsch.js +2 -0
- package/dist/plausible-analytics.js +2 -0
- package/dist/posthog.js +2 -0
- package/dist/promptwatch.js +2 -0
- package/dist/reddit-pixel.js +2 -0
- package/dist/registry.js +2 -392
- package/dist/resolve.js +2 -33
- package/dist/rudderstack.js +2 -0
- package/dist/rybbit-analytics.js +2 -0
- package/dist/segment.js +2 -0
- package/dist/snapchat-pixel.js +2 -0
- package/dist/tiktok-pixel.js +2 -0
- package/dist/types.js +2 -16
- package/dist/umami-analytics.js +2 -0
- package/dist/vendors/_shared/attributes.js +2 -14
- package/dist/vendors/_shared/google-consent.js +2 -27
- package/dist/vendors/_shared/install-builders.js +2 -21
- package/dist/vendors/_shared/required-id.js +2 -0
- package/dist/vendors/_shared/script-url.js +2 -28
- package/dist/vendors/ads-and-pixels/linkedin-insights.js +2 -48
- package/dist/vendors/ads-and-pixels/meta-pixel.js +2 -153
- package/dist/vendors/ads-and-pixels/microsoft-uet.js +2 -110
- package/dist/vendors/ads-and-pixels/openai-pixel.js +2 -88
- package/dist/vendors/ads-and-pixels/pinterest-tag.js +2 -0
- package/dist/vendors/ads-and-pixels/reddit-pixel.js +2 -107
- package/dist/vendors/ads-and-pixels/snapchat-pixel.js +2 -87
- package/dist/vendors/ads-and-pixels/tiktok-pixel.js +2 -89
- package/dist/vendors/ads-and-pixels/x-pixel.js +2 -48
- package/dist/vendors/analytics/adobe-analytics.js +2 -49
- package/dist/vendors/analytics/ahrefs-analytics.js +2 -27
- package/dist/vendors/analytics/amplitude.js +2 -134
- package/dist/vendors/analytics/clearbit.js +2 -28
- package/dist/vendors/analytics/cloudflare-web-analytics.js +2 -32
- package/dist/vendors/analytics/databuddy.js +2 -103
- package/dist/vendors/analytics/fathom-analytics.js +2 -35
- package/dist/vendors/analytics/google-tag.js +2 -66
- package/dist/vendors/analytics/heap.js +2 -134
- package/dist/vendors/analytics/hightouch.js +2 -109
- package/dist/vendors/analytics/hotjar.js +2 -44
- package/dist/vendors/analytics/logrocket.js +2 -58
- package/dist/vendors/analytics/matomo-analytics.js +2 -191
- package/dist/vendors/analytics/microsoft-clarity.js +2 -100
- package/dist/vendors/analytics/mixpanel-analytics.js +2 -93
- package/dist/vendors/analytics/one-dollar-stats.js +2 -0
- package/dist/vendors/analytics/pirsch.js +2 -67
- package/dist/vendors/analytics/plausible-analytics.js +2 -81
- package/dist/vendors/analytics/posthog.js +2 -200
- package/dist/vendors/analytics/promptwatch.js +2 -29
- package/dist/vendors/analytics/rudderstack.js +2 -183
- package/dist/vendors/analytics/rybbit-analytics.js +2 -63
- package/dist/vendors/analytics/segment.js +2 -56
- package/dist/vendors/analytics/umami-analytics.js +2 -39
- package/dist/vendors/analytics/vercel-analytics.js +2 -53
- package/dist/vendors/email-and-sms/klaviyo.js +2 -0
- package/dist/vendors/functional/crisp.js +2 -100
- package/dist/vendors/functional/front-chat.js +2 -0
- package/dist/vendors/functional/intercom.js +2 -45
- package/dist/vendors/tag-managers/cloudflare-zaraz.js +2 -98
- package/dist/vendors/tag-managers/google-tag-manager.js +2 -59
- package/dist/vercel-analytics.js +2 -0
- package/dist/x-pixel.js +2 -0
- package/dist-types/adobe-analytics.d.ts +2 -0
- package/dist-types/ahrefs-analytics.d.ts +2 -0
- package/dist-types/amplitude.d.ts +2 -0
- package/dist-types/clearbit.d.ts +2 -0
- package/dist-types/cloudflare-web-analytics.d.ts +2 -0
- package/dist-types/cloudflare-zaraz.d.ts +2 -0
- package/dist-types/crisp.d.ts +2 -0
- package/dist-types/databuddy.d.ts +2 -0
- package/dist-types/e2e-test-utils.d.ts +2 -0
- package/dist-types/engine/compile.d.ts +2 -3
- package/dist-types/engine/runtime.d.ts +2 -3
- package/dist-types/events.d.ts +2 -0
- package/dist-types/fathom-analytics.d.ts +2 -0
- package/dist-types/front-chat.d.ts +2 -0
- package/dist-types/google-tag-manager.d.ts +2 -0
- package/dist-types/google-tag.d.ts +2 -0
- package/dist-types/heap.d.ts +2 -0
- package/dist-types/hightouch.d.ts +2 -0
- package/dist-types/hotjar.d.ts +2 -0
- package/dist-types/intercom.d.ts +2 -0
- package/dist-types/klaviyo.d.ts +2 -0
- package/dist-types/linkedin-insights.d.ts +2 -0
- package/dist-types/logrocket.d.ts +2 -0
- package/dist-types/matomo-analytics.d.ts +2 -0
- package/dist-types/meta-pixel.d.ts +2 -0
- package/dist-types/microsoft-clarity.d.ts +2 -0
- package/dist-types/microsoft-uet.d.ts +2 -0
- package/dist-types/mixpanel-analytics.d.ts +2 -0
- package/dist-types/one-dollar-stats.d.ts +2 -0
- package/dist-types/openai-pixel.d.ts +2 -0
- package/dist-types/pinterest-tag.d.ts +2 -0
- package/dist-types/pirsch.d.ts +2 -0
- package/dist-types/plausible-analytics.d.ts +2 -0
- package/dist-types/posthog.d.ts +2 -0
- package/dist-types/promptwatch.d.ts +2 -0
- package/dist-types/reddit-pixel.d.ts +2 -0
- package/dist-types/registry.d.ts +2 -458
- package/dist-types/resolve.d.ts +2 -9
- package/dist-types/rudderstack.d.ts +2 -0
- package/dist-types/rybbit-analytics.d.ts +2 -0
- package/dist-types/segment.d.ts +2 -0
- package/dist-types/snapchat-pixel.d.ts +2 -0
- package/dist-types/tiktok-pixel.d.ts +2 -0
- package/dist-types/types.d.ts +2 -314
- package/dist-types/umami-analytics.d.ts +2 -0
- package/dist-types/vendors/_shared/attributes.d.ts +2 -35
- package/dist-types/vendors/_shared/google-consent.d.ts +2 -47
- package/dist-types/vendors/_shared/install-builders.d.ts +2 -30
- package/dist-types/vendors/_shared/required-id.d.ts +2 -0
- package/dist-types/vendors/_shared/script-url.d.ts +2 -75
- package/dist-types/vendors/ads-and-pixels/linkedin-insights.d.ts +2 -92
- package/dist-types/vendors/ads-and-pixels/meta-pixel.d.ts +2 -289
- package/dist-types/vendors/ads-and-pixels/microsoft-uet.d.ts +2 -105
- package/dist-types/vendors/ads-and-pixels/openai-pixel.d.ts +2 -211
- package/dist-types/vendors/ads-and-pixels/pinterest-tag.d.ts +2 -0
- package/dist-types/vendors/ads-and-pixels/reddit-pixel.d.ts +2 -210
- package/dist-types/vendors/ads-and-pixels/snapchat-pixel.d.ts +2 -171
- package/dist-types/vendors/ads-and-pixels/tiktok-pixel.d.ts +2 -106
- package/dist-types/vendors/ads-and-pixels/x-pixel.d.ts +2 -183
- package/dist-types/vendors/analytics/adobe-analytics.d.ts +2 -75
- package/dist-types/vendors/analytics/ahrefs-analytics.d.ts +2 -62
- package/dist-types/vendors/analytics/amplitude.d.ts +2 -234
- package/dist-types/vendors/analytics/clearbit.d.ts +2 -60
- package/dist-types/vendors/analytics/cloudflare-web-analytics.d.ts +2 -67
- package/dist-types/vendors/analytics/databuddy.d.ts +2 -147
- package/dist-types/vendors/analytics/fathom-analytics.d.ts +2 -90
- package/dist-types/vendors/analytics/google-tag.d.ts +2 -93
- package/dist-types/vendors/analytics/heap.d.ts +2 -316
- package/dist-types/vendors/analytics/hightouch.d.ts +2 -285
- package/dist-types/vendors/analytics/hotjar.d.ts +2 -73
- package/dist-types/vendors/analytics/logrocket.d.ts +2 -101
- package/dist-types/vendors/analytics/matomo-analytics.d.ts +2 -41
- package/dist-types/vendors/analytics/microsoft-clarity.d.ts +2 -97
- package/dist-types/vendors/analytics/mixpanel-analytics.d.ts +2 -113
- package/dist-types/vendors/analytics/one-dollar-stats.d.ts +2 -0
- package/dist-types/vendors/analytics/pirsch.d.ts +2 -96
- package/dist-types/vendors/analytics/plausible-analytics.d.ts +2 -122
- package/dist-types/vendors/analytics/posthog.d.ts +2 -175
- package/dist-types/vendors/analytics/promptwatch.d.ts +2 -36
- package/dist-types/vendors/analytics/rudderstack.d.ts +2 -330
- package/dist-types/vendors/analytics/rybbit-analytics.d.ts +2 -82
- package/dist-types/vendors/analytics/segment.d.ts +2 -158
- package/dist-types/vendors/analytics/umami-analytics.d.ts +2 -93
- package/dist-types/vendors/analytics/vercel-analytics.d.ts +2 -66
- package/dist-types/vendors/email-and-sms/klaviyo.d.ts +2 -0
- package/dist-types/vendors/functional/crisp.d.ts +2 -78
- package/dist-types/vendors/functional/front-chat.d.ts +2 -0
- package/dist-types/vendors/functional/intercom.d.ts +2 -135
- package/dist-types/vendors/tag-managers/cloudflare-zaraz.d.ts +2 -39
- package/dist-types/vendors/tag-managers/google-tag-manager.d.ts +2 -94
- package/dist-types/vercel-analytics.d.ts +2 -0
- package/dist-types/x-pixel.d.ts +2 -0
- package/docs/README.md +129 -59
- 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/concepts/consent-categories.md +84 -0
- 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/concepts/policies.md +71 -0
- 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/astro/embeds.md +160 -0
- package/docs/frameworks/astro/network-blocker.md +86 -0
- package/docs/frameworks/astro/scripts.md +146 -0
- package/docs/frameworks/html/embeds.md +142 -0
- package/docs/frameworks/html/network-blocker.md +105 -0
- package/docs/frameworks/html/scripts.md +164 -0
- package/docs/frameworks/javascript/scripts.md +119 -0
- package/docs/frameworks/next/embeds.md +90 -0
- package/docs/frameworks/next/network-blocker.md +153 -0
- package/docs/frameworks/next/scripts.md +196 -0
- package/docs/frameworks/nuxt/embeds.md +81 -0
- package/docs/frameworks/nuxt/network-blocker.md +97 -0
- package/docs/frameworks/nuxt/scripts.md +89 -0
- package/docs/frameworks/react/embeds.md +89 -0
- package/docs/frameworks/react/network-blocker.md +140 -0
- package/docs/frameworks/react/scripts.md +115 -0
- package/docs/frameworks/svelte/embeds.md +96 -0
- package/docs/frameworks/svelte/network-blocker.md +141 -0
- package/docs/frameworks/svelte/scripts.md +137 -0
- package/docs/frameworks/sveltekit/embeds.md +103 -0
- package/docs/frameworks/sveltekit/network-blocker.md +149 -0
- package/docs/frameworks/sveltekit/scripts.md +141 -0
- package/docs/frameworks/tanstack-start/embeds.md +96 -0
- package/docs/frameworks/tanstack-start/network-blocker.md +145 -0
- package/docs/frameworks/tanstack-start/scripts.md +103 -0
- package/docs/frameworks/vue/embeds.md +84 -0
- package/docs/frameworks/vue/network-blocker.md +99 -0
- package/docs/frameworks/vue/scripts.md +93 -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 +12 -236
- package/dist-types/__tests__/helpers.d.ts +0 -141
- 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/javascript/script-loader.md +0 -94
- package/docs/frameworks/next/script-loader.md +0 -210
- package/docs/frameworks/react/script-loader.md +0 -63
- 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
|
@@ -1,79 +1,871 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Banner
|
|
3
|
-
description:
|
|
4
|
-
|
|
2
|
+
title: Banner designs
|
|
3
|
+
description: Five consent banner designs built with c15t, from one prop to your
|
|
4
|
+
own markup, with tested code for React, Vue, Svelte, Astro and plain HTML.
|
|
5
5
|
group: customization
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Pick a design
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
Each design below is a real c15t banner. The screenshots and the code come from
|
|
11
|
+
the same Storybook stories, which CI runs to check that each design renders and
|
|
12
|
+
that Accept and Reject record a choice.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
| Design | Customization level | Frameworks |
|
|
15
|
+
| ----------------------------- | ----------------------- | ------------------------- |
|
|
16
|
+
| [Bottom bar](#bottom-bar) | Props | React, Vue, Astro, Svelte |
|
|
17
|
+
| [Branded card](#branded-card) | Theme tokens | React |
|
|
18
|
+
| [Choice wall](#choice-wall) | Props | React |
|
|
19
|
+
| [Slim bar](#slim-bar) | Compound parts or slots | React, Vue, Svelte |
|
|
20
|
+
| [Headless bar](#headless-bar) | Headless | React, Vue, Svelte, HTML |
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
Next.js and TanStack Start apps use the React code, with the same components
|
|
23
|
+
imported from `c15t/next` or `c15t/tanstack-start`. Nuxt uses the Vue code and
|
|
24
|
+
SvelteKit uses the Svelte code. [Customization](./overview.md)
|
|
25
|
+
explains when to use each level.
|
|
26
|
+
|
|
27
|
+
Every design keeps the policy in charge. The policy decides which buttons a
|
|
28
|
+
visitor gets, and none of these designs records consent until the visitor
|
|
29
|
+
clicks a button.
|
|
30
|
+
|
|
31
|
+
## Bottom bar
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
The stock banner becomes a full-width bar along the bottom edge with one prop,
|
|
36
|
+
`variant="bar"`. The title and copy sit on the left and the buttons on the
|
|
37
|
+
right.
|
|
38
|
+
|
|
39
|
+
**React**
|
|
40
|
+
|
|
41
|
+
Render `CookieBanner` in place of `ConsentBanner`, inside your
|
|
42
|
+
`ConsentProvider` and next to `ConsentDialog`.
|
|
43
|
+
|
|
44
|
+
```tsx title="src/cookie-banner.tsx"
|
|
45
|
+
import { ConsentBanner } from 'c15t/react';
|
|
46
|
+
|
|
47
|
+
/** The stock banner, drawn as a full-width bar along the bottom edge. */
|
|
48
|
+
export const CookieBanner = () => (
|
|
49
|
+
<ConsentBanner variant="bar" position="bottom" />
|
|
50
|
+
);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Vue**
|
|
54
|
+
|
|
55
|
+
Render `CookieBanner` and `ConsentManager` from
|
|
56
|
+
`c15t/vue/runtime/components/consent-manager.vue` in place of
|
|
57
|
+
`ConsentRoot`. To keep `ConsentRoot`, set `presentation.prompt` in the
|
|
58
|
+
`c15tVue` options instead, as in [customize](https://c15t.com/docs/frameworks/vue/customize).
|
|
59
|
+
|
|
60
|
+
```vue title="src/CookieBanner.vue"
|
|
61
|
+
<script setup lang="ts">
|
|
62
|
+
import ConsentBanner from 'c15t/vue/runtime/components/consent-banner.vue';
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<!-- The stock banner, drawn as a full-width bar along the bottom edge. -->
|
|
67
|
+
<ConsentBanner variant="bar" position="bottom" />
|
|
68
|
+
</template>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Astro**
|
|
72
|
+
|
|
73
|
+
Add `presentation` to the `c15t()` options in `astro.config.mjs`. The
|
|
74
|
+
server renders the banner as a bar.
|
|
75
|
+
|
|
76
|
+
```ts title="astro.config.mjs (partial)"
|
|
77
|
+
presentation: {
|
|
78
|
+
prompt: { position: 'bottom', variant: 'bar' },
|
|
79
|
+
},
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Svelte**
|
|
83
|
+
|
|
84
|
+
Render `CookieBanner` in place of `ConsentBanner`, inside
|
|
85
|
+
`ConsentManagerProvider` and next to `ConsentDialog`.
|
|
86
|
+
|
|
87
|
+
```svelte title="src/lib/cookie-banner.svelte"
|
|
88
|
+
<script lang="ts">
|
|
89
|
+
import { ConsentBanner } from '@c15t/svelte';
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<!-- The stock banner, drawn as a full-width bar along the bottom edge. -->
|
|
93
|
+
<ConsentBanner variant="bar" position="bottom" />
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The HTML script tag takes the same shape through `presentation.prompt`. See
|
|
97
|
+
[customize the script tag](https://c15t.com/docs/frameworks/html/customize#change-the-banners-shape-and-position).
|
|
98
|
+
|
|
99
|
+
## Branded card
|
|
100
|
+
|
|
101
|
+

|
|
102
|
+
|
|
103
|
+
Theme tokens change the colors, type and radius of the stock card without
|
|
104
|
+
touching its markup. This theme sets a green primary color, an off-white
|
|
105
|
+
surface, a serif font and four-pixel corners, and fills the actions the policy
|
|
106
|
+
marks as primary. Here the policy marks both Reject All and Accept All as
|
|
107
|
+
primary, so neither choice stands out.
|
|
108
|
+
|
|
109
|
+
```ts title="src/consent-theme.ts"
|
|
110
|
+
import { defineTheme } from 'c15t/react';
|
|
18
111
|
|
|
19
112
|
export const brandTheme = defineTheme({
|
|
20
|
-
colors: {
|
|
21
|
-
|
|
22
|
-
primary:
|
|
113
|
+
colors: {
|
|
114
|
+
border: '#e4dccf',
|
|
115
|
+
primary: '#2f6f4e',
|
|
116
|
+
primaryHover: '#24563c',
|
|
117
|
+
surface: '#fbf8f3',
|
|
118
|
+
textOnPrimary: '#ffffff',
|
|
23
119
|
},
|
|
24
|
-
|
|
120
|
+
consentActions: { primary: { mode: 'filled', variant: 'primary' } },
|
|
121
|
+
radius: { lg: '4px', md: '4px' },
|
|
122
|
+
typography: { fontFamily: 'Georgia, "Times New Roman", serif' },
|
|
25
123
|
});
|
|
26
|
-
|
|
27
124
|
```
|
|
28
125
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
configuration. The theme source above is also imported by the runnable example,
|
|
32
|
-
so the rendered result and the documentation use the same values.
|
|
126
|
+
```tsx title="src/consent-ui.tsx"
|
|
127
|
+
import { ConsentBanner, ConsentDialog, ConsentTheme } from 'c15t/react';
|
|
33
128
|
|
|
34
|
-
|
|
129
|
+
import { brandTheme } from './consent-theme';
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Render inside your ConsentProvider, and pass `theme: brandTheme` in the
|
|
133
|
+
* provider options so the action styles apply too.
|
|
134
|
+
*/
|
|
135
|
+
export const ConsentUI = () => (
|
|
136
|
+
<>
|
|
137
|
+
<ConsentTheme theme={brandTheme} />
|
|
138
|
+
<ConsentBanner />
|
|
139
|
+
<ConsentDialog />
|
|
140
|
+
</>
|
|
141
|
+
);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`ConsentTheme` renders the tokens as a `<style>` element. In a server-rendered
|
|
145
|
+
app, render it from a Server Component or the root layout so the tokens arrive
|
|
146
|
+
with the HTML. The provider's `theme` option reads only `consentActions` and
|
|
147
|
+
slot styles, so pass the same object there for the filled buttons:
|
|
35
148
|
|
|
36
149
|
```tsx
|
|
37
|
-
<
|
|
150
|
+
<ConsentProvider options={{ mode, theme: brandTheme }}>
|
|
38
151
|
```
|
|
39
152
|
|
|
40
|
-
|
|
153
|
+
The preferences dialog reads the same tokens:
|
|
41
154
|
|
|
42
|
-
|
|
43
|
-
or require a headless implementation.
|
|
155
|
+

|
|
44
156
|
|
|
45
|
-
|
|
46
|
-
|
|
157
|
+
Vue, Svelte, Astro and the script tag set the same tokens their own way. See
|
|
158
|
+
customize for [Vue](https://c15t.com/docs/frameworks/vue/customize),
|
|
159
|
+
[Svelte](https://c15t.com/docs/frameworks/svelte/customize),
|
|
160
|
+
[Astro](https://c15t.com/docs/frameworks/astro/customize) or
|
|
161
|
+
[HTML](https://c15t.com/docs/frameworks/html/customize), and
|
|
162
|
+
[theme tokens](./tokens.md) for every token.
|
|
163
|
+
|
|
164
|
+
## Choice wall
|
|
165
|
+
|
|
166
|
+

|
|
167
|
+
|
|
168
|
+
`variant="wall"` centers the stock banner over a backdrop, locks scrolling and
|
|
169
|
+
traps focus until the visitor chooses. Use it only where a policy requires a
|
|
170
|
+
choice before the page is usable.
|
|
171
|
+
|
|
172
|
+
```tsx title="src/cookie-banner.tsx"
|
|
173
|
+
import { ConsentBanner } from 'c15t/react';
|
|
174
|
+
|
|
175
|
+
/** A centered prompt with a backdrop. The page waits for a choice. */
|
|
176
|
+
export const CookieBanner = () => <ConsentBanner variant="wall" />;
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A choice wall always blocks. A notice never blocks, so a wall requested for a
|
|
180
|
+
notice falls back to the floating card.
|
|
181
|
+
|
|
182
|
+
## Slim bar
|
|
183
|
+
|
|
184
|
+

|
|
185
|
+
|
|
186
|
+
The slim bar puts the title, the copy and the buttons on one row, so it is
|
|
187
|
+
shorter than the stock bar. React builds it from the
|
|
188
|
+
`ConsentBanner` compound parts. Vue and Svelte have no compound banner parts,
|
|
189
|
+
so they add classes to the stock bar's card and header through slots. All
|
|
190
|
+
three use the same stylesheet, and all three keep the buttons the policy
|
|
191
|
+
requires.
|
|
192
|
+
|
|
193
|
+
**React**
|
|
194
|
+
|
|
195
|
+
`ConsentBanner.PolicyActions` renders the policy's buttons, so a required
|
|
196
|
+
Reject button stays. Render `CookieBanner` inside your `ConsentProvider`,
|
|
197
|
+
next to `ConsentDialog`.
|
|
198
|
+
|
|
199
|
+
```tsx title="src/cookie-banner.tsx"
|
|
200
|
+
import { ConsentBanner } from 'c15t/react';
|
|
201
|
+
|
|
202
|
+
import './slim-bar.css';
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The title, copy and the policy's buttons in a single row. The compound
|
|
206
|
+
* parts keep the policy's actions, so a required Reject button stays.
|
|
207
|
+
*/
|
|
208
|
+
export const CookieBanner = () => (
|
|
209
|
+
<ConsentBanner.Root variant="bar">
|
|
210
|
+
<ConsentBanner.Card className="slim-bar">
|
|
211
|
+
<ConsentBanner.Header className="slim-bar__text">
|
|
212
|
+
<ConsentBanner.Title />
|
|
213
|
+
<ConsentBanner.Description />
|
|
214
|
+
</ConsentBanner.Header>
|
|
215
|
+
<ConsentBanner.PolicyActions />
|
|
216
|
+
</ConsentBanner.Card>
|
|
217
|
+
</ConsentBanner.Root>
|
|
218
|
+
);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Vue**
|
|
222
|
+
|
|
223
|
+
Spread `slimBar` into the options you pass to `app.use(c15tVue, ...)`.
|
|
224
|
+
`ConsentRoot` then renders the slim bar.
|
|
225
|
+
|
|
226
|
+
```ts title="src/consent-options.ts"
|
|
227
|
+
import type { C15tVuePluginOptions } from 'c15t/vue/vue-plugin';
|
|
228
|
+
|
|
229
|
+
import './slim-bar.css';
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Spread into the options you pass to `app.use(c15tVue, ...)`. The bar keeps
|
|
233
|
+
* the stock markup and actions; the slot classes put it on one row.
|
|
234
|
+
*/
|
|
235
|
+
export const slimBar = {
|
|
236
|
+
components: {
|
|
237
|
+
banner: {
|
|
238
|
+
card: { class: 'slim-bar' },
|
|
239
|
+
header: { class: 'slim-bar__text' },
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
presentation: { prompt: { position: 'bottom', variant: 'bar' } },
|
|
243
|
+
} satisfies C15tVuePluginOptions;
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Svelte**
|
|
247
|
+
|
|
248
|
+
Spread `slimBar` into the options you pass to `ConsentManagerProvider`.
|
|
249
|
+
Svelte scopes component styles, so the classes live in a global
|
|
250
|
+
stylesheet.
|
|
251
|
+
|
|
252
|
+
```ts title="src/lib/consent-options.ts"
|
|
253
|
+
import type { ConsentManagerOptions } from '@c15t/svelte';
|
|
254
|
+
|
|
255
|
+
import './slim-bar.css';
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Spread into the options you pass to `ConsentManagerProvider`. The bar
|
|
259
|
+
* keeps the stock markup and actions; the slot classes put it on one row.
|
|
260
|
+
*/
|
|
261
|
+
export const slimBar = {
|
|
262
|
+
presentation: { prompt: { position: 'bottom', variant: 'bar' } },
|
|
263
|
+
theme: {
|
|
264
|
+
slots: {
|
|
265
|
+
consentBannerCard: 'slim-bar',
|
|
266
|
+
consentBannerHeader: 'slim-bar__text',
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
} satisfies Partial<ConsentManagerOptions>;
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
```css title="src/slim-bar.css"
|
|
273
|
+
.slim-bar {
|
|
274
|
+
box-sizing: border-box;
|
|
275
|
+
display: flex;
|
|
276
|
+
flex-wrap: wrap;
|
|
277
|
+
align-items: center;
|
|
278
|
+
gap: 0.5rem 1.5rem;
|
|
279
|
+
padding: 0.5rem 1.5rem;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Title and description share one line. */
|
|
283
|
+
.slim-bar__text {
|
|
284
|
+
display: flex;
|
|
285
|
+
flex: 1 1 20rem;
|
|
286
|
+
flex-direction: row;
|
|
287
|
+
flex-wrap: wrap;
|
|
288
|
+
align-items: baseline;
|
|
289
|
+
gap: 0 0.5rem;
|
|
290
|
+
padding: 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* The actions share the text's row, so drop the footer's own box. */
|
|
294
|
+
.slim-bar > [data-testid='consent-banner-footer'] {
|
|
295
|
+
padding: 0;
|
|
296
|
+
border: 0;
|
|
297
|
+
background: none;
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Headless bar
|
|
302
|
+
|
|
303
|
+

|
|
304
|
+
|
|
305
|
+
The headless bar is your own markup and CSS, driven by c15t's headless API. It
|
|
306
|
+
shows the same design in every framework. The policy still decides which
|
|
307
|
+
buttons appear and in what order, the Preferences button is always there, and
|
|
308
|
+
a choice is recorded only when the visitor clicks.
|
|
309
|
+
|
|
310
|
+
**React**
|
|
311
|
+
|
|
312
|
+
`useHeadlessConsentUI()` from `c15t/react/headless` returns the banner
|
|
313
|
+
state and `performAction`. Render `CookieBar` in place of
|
|
314
|
+
`ConsentBanner`, and keep `ConsentDialog`, which Preferences opens.
|
|
315
|
+
|
|
316
|
+
```tsx title="src/cookie-bar.tsx"
|
|
317
|
+
import { useHeadlessConsentUI } from 'c15t/react/headless';
|
|
318
|
+
import type { HeadlessConsentBannerAction } from 'c15t/react/headless';
|
|
319
|
+
|
|
320
|
+
import './cookie-bar.css';
|
|
321
|
+
|
|
322
|
+
const labels: Record<HeadlessConsentBannerAction, string> = {
|
|
323
|
+
accept: 'Accept all',
|
|
324
|
+
customize: 'Preferences',
|
|
325
|
+
dismiss: 'Got it',
|
|
326
|
+
reject: 'Reject all',
|
|
327
|
+
save: 'Save',
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
export const CookieBar = () => {
|
|
331
|
+
const { banner, openDialog, performAction } = useHeadlessConsentUI();
|
|
332
|
+
|
|
333
|
+
if (!banner.isVisible) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// The policy decides which actions this visitor gets, and in what order.
|
|
338
|
+
// Preferences has its own button, so it is always there.
|
|
339
|
+
const actions = banner.orderedActions.filter(
|
|
340
|
+
(action) => action !== 'customize'
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<section className="cookie-bar" aria-label="Cookie consent">
|
|
345
|
+
<p className="cookie-bar__text">
|
|
346
|
+
We use cookies to measure traffic and improve this site. Choose which
|
|
347
|
+
ones can run.
|
|
348
|
+
</p>
|
|
349
|
+
<div className="cookie-bar__actions">
|
|
350
|
+
<button type="button" className="cookie-bar__link" onClick={openDialog}>
|
|
351
|
+
Preferences
|
|
352
|
+
</button>
|
|
353
|
+
{actions.map((action) => (
|
|
354
|
+
<button
|
|
355
|
+
key={action}
|
|
356
|
+
type="button"
|
|
357
|
+
className="cookie-bar__button"
|
|
358
|
+
onClick={() => performAction(action)}
|
|
359
|
+
>
|
|
360
|
+
{labels[action]}
|
|
361
|
+
</button>
|
|
362
|
+
))}
|
|
363
|
+
</div>
|
|
364
|
+
</section>
|
|
365
|
+
);
|
|
366
|
+
};
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Vue**
|
|
370
|
+
|
|
371
|
+
The composables from `c15t/vue/vue-plugin` give the policy's actions and
|
|
372
|
+
the active surface. Render `CookieBar` and `ConsentManager` in place of
|
|
373
|
+
`ConsentRoot`. Preferences sets `activeUI` to `'manager'`, which opens
|
|
374
|
+
`ConsentManager`.
|
|
375
|
+
|
|
376
|
+
```vue title="src/CookieBar.vue"
|
|
377
|
+
<script setup lang="ts">
|
|
378
|
+
import {
|
|
379
|
+
useConsentActiveUI,
|
|
380
|
+
useConsentPolicyActions,
|
|
381
|
+
useConsentSave,
|
|
382
|
+
useDismissNotice,
|
|
383
|
+
usePromptRequirement,
|
|
384
|
+
} from 'c15t/vue/vue-plugin';
|
|
385
|
+
import { computed } from 'vue';
|
|
386
|
+
|
|
387
|
+
import './cookie-bar.css';
|
|
388
|
+
|
|
389
|
+
const labels: Record<string, string> = {
|
|
390
|
+
accept: 'Accept all',
|
|
391
|
+
dismiss: 'Got it',
|
|
392
|
+
reject: 'Reject all',
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const activeUI = useConsentActiveUI();
|
|
396
|
+
const prompt = usePromptRequirement();
|
|
397
|
+
const { actionGroups } = useConsentPolicyActions('prompt');
|
|
398
|
+
const saveConsent = useConsentSave();
|
|
399
|
+
const dismissNotice = useDismissNotice();
|
|
400
|
+
|
|
401
|
+
const isVisible = computed(
|
|
402
|
+
() => activeUI.value === 'banner' && prompt.value.kind !== 'none'
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
// The policy decides which actions this visitor gets, and in what order.
|
|
406
|
+
// Preferences has its own button, so it is always there.
|
|
407
|
+
const actions = computed(() =>
|
|
408
|
+
actionGroups.value.flat().filter((action) => action in labels)
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const run = (action: string) => {
|
|
412
|
+
if (action === 'accept') {
|
|
413
|
+
void saveConsent('all');
|
|
414
|
+
} else if (action === 'reject') {
|
|
415
|
+
void saveConsent('none');
|
|
416
|
+
} else if (action === 'dismiss') {
|
|
417
|
+
void dismissNotice();
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
</script>
|
|
421
|
+
|
|
422
|
+
<template>
|
|
423
|
+
<section v-if="isVisible" class="cookie-bar" aria-label="Cookie consent">
|
|
424
|
+
<p class="cookie-bar__text">
|
|
425
|
+
We use cookies to measure traffic and improve this site. Choose which ones
|
|
426
|
+
can run.
|
|
427
|
+
</p>
|
|
428
|
+
<div class="cookie-bar__actions">
|
|
429
|
+
<button
|
|
430
|
+
type="button"
|
|
431
|
+
class="cookie-bar__link"
|
|
432
|
+
@click="activeUI = 'manager'"
|
|
433
|
+
>
|
|
434
|
+
Preferences
|
|
435
|
+
</button>
|
|
436
|
+
<button
|
|
437
|
+
v-for="action in actions"
|
|
438
|
+
:key="action"
|
|
439
|
+
type="button"
|
|
440
|
+
class="cookie-bar__button"
|
|
441
|
+
@click="run(action)"
|
|
442
|
+
>
|
|
443
|
+
{{ labels[action] }}
|
|
444
|
+
</button>
|
|
445
|
+
</div>
|
|
446
|
+
</section>
|
|
447
|
+
</template>
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Svelte**
|
|
451
|
+
|
|
452
|
+
`getHeadlessConsent()` returns the banner state and `performAction`.
|
|
453
|
+
Render `CookieBar` in place of `ConsentBanner`, inside
|
|
454
|
+
`ConsentManagerProvider`, and keep `ConsentDialog`, which Preferences
|
|
455
|
+
opens.
|
|
456
|
+
|
|
457
|
+
```svelte title="src/lib/cookie-bar.svelte"
|
|
458
|
+
<script lang="ts">
|
|
459
|
+
import { getHeadlessConsent } from '@c15t/svelte';
|
|
460
|
+
import type { PresentationAction } from '@c15t/svelte';
|
|
461
|
+
|
|
462
|
+
import './cookie-bar.css';
|
|
463
|
+
|
|
464
|
+
const consent = getHeadlessConsent();
|
|
465
|
+
|
|
466
|
+
const labels: Record<PresentationAction, string> = {
|
|
467
|
+
accept: 'Accept all',
|
|
468
|
+
customize: 'Preferences',
|
|
469
|
+
dismiss: 'Got it',
|
|
470
|
+
reject: 'Reject all',
|
|
471
|
+
save: 'Save',
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
// The policy decides which actions this visitor gets, and in what order.
|
|
475
|
+
// Preferences has its own button, so it is always there.
|
|
476
|
+
const actions = $derived(
|
|
477
|
+
consent.banner.orderedActions.filter((action) => action !== 'customize')
|
|
478
|
+
);
|
|
479
|
+
</script>
|
|
480
|
+
|
|
481
|
+
{#if consent.banner.isVisible}
|
|
482
|
+
<section class="cookie-bar" aria-label="Cookie consent">
|
|
483
|
+
<p class="cookie-bar__text">
|
|
484
|
+
We use cookies to measure traffic and improve this site. Choose which ones
|
|
485
|
+
can run.
|
|
486
|
+
</p>
|
|
487
|
+
<div class="cookie-bar__actions">
|
|
488
|
+
<button
|
|
489
|
+
type="button"
|
|
490
|
+
class="cookie-bar__link"
|
|
491
|
+
onclick={() => consent.openDialog()}
|
|
492
|
+
>
|
|
493
|
+
Preferences
|
|
494
|
+
</button>
|
|
495
|
+
{#each actions as action (action)}
|
|
496
|
+
<button
|
|
497
|
+
type="button"
|
|
498
|
+
class="cookie-bar__button"
|
|
499
|
+
onclick={() => consent.performAction(action)}
|
|
500
|
+
>
|
|
501
|
+
{labels[action]}
|
|
502
|
+
</button>
|
|
503
|
+
{/each}
|
|
504
|
+
</div>
|
|
505
|
+
</section>
|
|
506
|
+
{/if}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**HTML**
|
|
510
|
+
|
|
511
|
+
The headless build, `c15t.headless.js`, keeps script gating and
|
|
512
|
+
`window.c15t` and ships no banner, dialog or CSS. Buttons with
|
|
513
|
+
`data-c15t-action` need no code. Because there is no stock dialog,
|
|
514
|
+
Preferences opens category switches inside the bar and a Save button
|
|
515
|
+
records them. Replace `https://your-project.inth.app` with the backend URL
|
|
516
|
+
from your Inth project, and put the block before `</body>`.
|
|
517
|
+
|
|
518
|
+
```html title="index.html"
|
|
519
|
+
<script
|
|
520
|
+
src="https://cdn.jsdelivr.net/npm/@c15t/browser@alpha/dist/c15t.headless.js"
|
|
521
|
+
data-backend-url="https://your-project.inth.app"
|
|
522
|
+
defer
|
|
523
|
+
></script>
|
|
524
|
+
|
|
525
|
+
<style>
|
|
526
|
+
.cookie-bar {
|
|
527
|
+
position: fixed;
|
|
528
|
+
inset: auto 0 0;
|
|
529
|
+
z-index: 50;
|
|
530
|
+
display: flex;
|
|
531
|
+
flex-wrap: wrap;
|
|
532
|
+
align-items: center;
|
|
533
|
+
gap: 0.75rem 1.5rem;
|
|
534
|
+
padding: 0.875rem 1.5rem;
|
|
535
|
+
background: #14213d;
|
|
536
|
+
color: #f8f9fb;
|
|
537
|
+
font:
|
|
538
|
+
0.9375rem/1.5 system-ui,
|
|
539
|
+
sans-serif;
|
|
540
|
+
box-shadow: 0 -4px 16px rgb(0 0 0 / 0.15);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.cookie-bar__text {
|
|
544
|
+
flex: 1 1 20rem;
|
|
545
|
+
margin: 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.cookie-bar__actions {
|
|
549
|
+
display: flex;
|
|
550
|
+
flex-wrap: wrap;
|
|
551
|
+
gap: 0.5rem;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.cookie-bar button {
|
|
555
|
+
padding: 0.5rem 1rem;
|
|
556
|
+
border: 1px solid #f8f9fb;
|
|
557
|
+
border-radius: 999px;
|
|
558
|
+
background: transparent;
|
|
559
|
+
color: inherit;
|
|
560
|
+
font: inherit;
|
|
561
|
+
cursor: pointer;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.cookie-bar .cookie-bar__button {
|
|
565
|
+
background: #f8f9fb;
|
|
566
|
+
color: #14213d;
|
|
567
|
+
font-weight: 600;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.cookie-bar .cookie-bar__link {
|
|
571
|
+
border-color: transparent;
|
|
572
|
+
text-decoration: underline;
|
|
573
|
+
text-underline-offset: 3px;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.cookie-bar button:focus-visible {
|
|
577
|
+
outline: 2px solid #fca311;
|
|
578
|
+
outline-offset: 2px;
|
|
579
|
+
}
|
|
580
|
+
/* On narrow screens, Reject and Accept share a row at equal width. */
|
|
581
|
+
@media (max-width: 30rem) {
|
|
582
|
+
.cookie-bar__actions {
|
|
583
|
+
width: 100%;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.cookie-bar__actions button {
|
|
587
|
+
flex: 1 1 40%;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.cookie-bar__actions .cookie-bar__link {
|
|
591
|
+
flex-basis: 100%;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/* The headless build has no dialog, so preferences open inside the bar. */
|
|
596
|
+
.cookie-bar[hidden],
|
|
597
|
+
.cookie-bar__prefs[hidden] {
|
|
598
|
+
display: none;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.cookie-bar__prefs {
|
|
602
|
+
display: flex;
|
|
603
|
+
flex-wrap: wrap;
|
|
604
|
+
gap: 0.5rem 1.25rem;
|
|
605
|
+
flex-basis: 100%;
|
|
606
|
+
margin: 0;
|
|
607
|
+
padding: 0;
|
|
608
|
+
border: 0;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.cookie-bar__prefs label {
|
|
612
|
+
display: flex;
|
|
613
|
+
align-items: center;
|
|
614
|
+
gap: 0.375rem;
|
|
615
|
+
}
|
|
616
|
+
</style>
|
|
617
|
+
|
|
618
|
+
<section
|
|
619
|
+
id="cookie-bar"
|
|
620
|
+
class="cookie-bar"
|
|
621
|
+
aria-label="Cookie consent"
|
|
622
|
+
hidden
|
|
623
|
+
>
|
|
624
|
+
<p class="cookie-bar__text">
|
|
625
|
+
We use cookies to measure traffic and improve this site. Choose which ones
|
|
626
|
+
can run.
|
|
627
|
+
</p>
|
|
628
|
+
<fieldset
|
|
629
|
+
class="cookie-bar__prefs"
|
|
630
|
+
hidden
|
|
631
|
+
>
|
|
632
|
+
<legend class="cookie-bar__text">Allow these cookies:</legend>
|
|
633
|
+
<label
|
|
634
|
+
><input
|
|
635
|
+
type="checkbox"
|
|
636
|
+
name="functionality"
|
|
637
|
+
/>
|
|
638
|
+
Functionality</label
|
|
639
|
+
>
|
|
640
|
+
<label
|
|
641
|
+
><input
|
|
642
|
+
type="checkbox"
|
|
643
|
+
name="measurement"
|
|
644
|
+
/>
|
|
645
|
+
Measurement</label
|
|
646
|
+
>
|
|
647
|
+
<label
|
|
648
|
+
><input
|
|
649
|
+
type="checkbox"
|
|
650
|
+
name="experience"
|
|
651
|
+
/>
|
|
652
|
+
Experience</label
|
|
653
|
+
>
|
|
654
|
+
<label
|
|
655
|
+
><input
|
|
656
|
+
type="checkbox"
|
|
657
|
+
name="marketing"
|
|
658
|
+
/>
|
|
659
|
+
Marketing</label
|
|
660
|
+
>
|
|
661
|
+
</fieldset>
|
|
662
|
+
<div class="cookie-bar__actions">
|
|
663
|
+
<!-- c15t wires data-c15t-action buttons itself; they need no code. -->
|
|
664
|
+
<button
|
|
665
|
+
type="button"
|
|
666
|
+
class="cookie-bar__link"
|
|
667
|
+
data-c15t-action="customize"
|
|
668
|
+
>
|
|
669
|
+
Preferences
|
|
670
|
+
</button>
|
|
671
|
+
<button
|
|
672
|
+
type="button"
|
|
673
|
+
class="cookie-bar__button"
|
|
674
|
+
data-c15t-action="dismiss"
|
|
675
|
+
data-notice-only
|
|
676
|
+
hidden
|
|
677
|
+
>
|
|
678
|
+
Got it
|
|
679
|
+
</button>
|
|
680
|
+
<button
|
|
681
|
+
type="button"
|
|
682
|
+
class="cookie-bar__button"
|
|
683
|
+
data-c15t-action="reject"
|
|
684
|
+
data-choice-only
|
|
685
|
+
>
|
|
686
|
+
Reject all
|
|
687
|
+
</button>
|
|
688
|
+
<button
|
|
689
|
+
type="button"
|
|
690
|
+
class="cookie-bar__button"
|
|
691
|
+
data-c15t-action="accept"
|
|
692
|
+
data-choice-only
|
|
693
|
+
>
|
|
694
|
+
Accept all
|
|
695
|
+
</button>
|
|
696
|
+
<button
|
|
697
|
+
type="button"
|
|
698
|
+
class="cookie-bar__button"
|
|
699
|
+
data-save
|
|
700
|
+
hidden
|
|
701
|
+
>
|
|
702
|
+
Save
|
|
703
|
+
</button>
|
|
704
|
+
</div>
|
|
705
|
+
</section>
|
|
706
|
+
|
|
707
|
+
<script>
|
|
708
|
+
const bar = document.getElementById('cookie-bar');
|
|
709
|
+
const prefs = bar.querySelector('.cookie-bar__prefs');
|
|
710
|
+
const saveButton = bar.querySelector('[data-save]');
|
|
711
|
+
const switches = prefs.querySelectorAll('input');
|
|
712
|
+
const choiceButtons = bar.querySelectorAll('[data-choice-only]');
|
|
713
|
+
const noticeButtons = bar.querySelectorAll('[data-notice-only]');
|
|
714
|
+
|
|
715
|
+
// The tag is deferred, so queue calls until window.c15t is the real API.
|
|
716
|
+
window.c15t = window.c15t || [];
|
|
717
|
+
const queue = (call) => {
|
|
718
|
+
if (Array.isArray(window.c15t)) {
|
|
719
|
+
window.c15t.push(call);
|
|
720
|
+
} else {
|
|
721
|
+
window.c15t[call[0]](...call.slice(1));
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// c15t says which surface is due; the page decides what it looks like.
|
|
726
|
+
// 'banner' shows the bar, 'dialog' opens the switches, 'none' hides it.
|
|
727
|
+
queue([
|
|
728
|
+
'on',
|
|
729
|
+
'ui',
|
|
730
|
+
(surface) => {
|
|
731
|
+
const { kind } = c15t.getSnapshot().promptRequirement;
|
|
732
|
+
const editing = surface === 'dialog';
|
|
733
|
+
// A notice gets one acknowledgement button instead of Accept and Reject.
|
|
734
|
+
for (const button of choiceButtons) {
|
|
735
|
+
button.hidden = kind === 'notice' && !editing;
|
|
736
|
+
}
|
|
737
|
+
for (const button of noticeButtons) {
|
|
738
|
+
button.hidden = kind !== 'notice' || editing;
|
|
739
|
+
}
|
|
740
|
+
for (const input of switches) {
|
|
741
|
+
input.checked = c15t.has(input.name);
|
|
742
|
+
}
|
|
743
|
+
prefs.hidden = !editing;
|
|
744
|
+
saveButton.hidden = !editing;
|
|
745
|
+
bar.hidden =
|
|
746
|
+
surface === 'none' || (surface === 'banner' && kind === 'none');
|
|
747
|
+
},
|
|
748
|
+
]);
|
|
749
|
+
|
|
750
|
+
// Save records exactly what the switches show.
|
|
751
|
+
saveButton.addEventListener('click', () => {
|
|
752
|
+
const choice = {};
|
|
753
|
+
for (const input of switches) {
|
|
754
|
+
choice[input.name] = input.checked;
|
|
755
|
+
}
|
|
756
|
+
c15t.save(choice);
|
|
757
|
+
});
|
|
758
|
+
</script>
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
React, Vue and Svelte import the same stylesheet, `cookie-bar.css`. The HTML
|
|
762
|
+
block contains it inline.
|
|
763
|
+
|
|
764
|
+
```css title="src/cookie-bar.css"
|
|
765
|
+
.cookie-bar {
|
|
766
|
+
position: fixed;
|
|
767
|
+
inset: auto 0 0;
|
|
768
|
+
z-index: 50;
|
|
769
|
+
display: flex;
|
|
770
|
+
flex-wrap: wrap;
|
|
771
|
+
align-items: center;
|
|
772
|
+
gap: 0.75rem 1.5rem;
|
|
773
|
+
padding: 0.875rem 1.5rem;
|
|
774
|
+
background: #14213d;
|
|
775
|
+
color: #f8f9fb;
|
|
776
|
+
font:
|
|
777
|
+
0.9375rem/1.5 system-ui,
|
|
778
|
+
sans-serif;
|
|
779
|
+
box-shadow: 0 -4px 16px rgb(0 0 0 / 0.15);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
.cookie-bar__text {
|
|
783
|
+
flex: 1 1 20rem;
|
|
784
|
+
margin: 0;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.cookie-bar__actions {
|
|
788
|
+
display: flex;
|
|
789
|
+
flex-wrap: wrap;
|
|
790
|
+
gap: 0.5rem;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.cookie-bar button {
|
|
794
|
+
padding: 0.5rem 1rem;
|
|
795
|
+
border: 1px solid #f8f9fb;
|
|
796
|
+
border-radius: 999px;
|
|
797
|
+
background: transparent;
|
|
798
|
+
color: inherit;
|
|
799
|
+
font: inherit;
|
|
800
|
+
cursor: pointer;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.cookie-bar .cookie-bar__button {
|
|
804
|
+
background: #f8f9fb;
|
|
805
|
+
color: #14213d;
|
|
806
|
+
font-weight: 600;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.cookie-bar .cookie-bar__link {
|
|
810
|
+
border-color: transparent;
|
|
811
|
+
text-decoration: underline;
|
|
812
|
+
text-underline-offset: 3px;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.cookie-bar button:focus-visible {
|
|
816
|
+
outline: 2px solid #fca311;
|
|
817
|
+
outline-offset: 2px;
|
|
818
|
+
}
|
|
819
|
+
/* On narrow screens, Reject and Accept share a row at equal width. */
|
|
820
|
+
@media (max-width: 30rem) {
|
|
821
|
+
.cookie-bar__actions {
|
|
822
|
+
width: 100%;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
.cookie-bar__actions button {
|
|
826
|
+
flex: 1 1 40%;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.cookie-bar__actions .cookie-bar__link {
|
|
830
|
+
flex-basis: 100%;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
47
833
|
```
|
|
48
834
|
|
|
49
|
-
|
|
835
|
+
On a 375-pixel screen the copy takes the full width, Preferences sits on its
|
|
836
|
+
own line, and Reject all and Accept all share a row at equal width:
|
|
50
837
|
|
|
51
|
-
|
|
52
|
-
a wall for a notice falls back to a non-blocking presentation.
|
|
838
|
+

|
|
53
839
|
|
|
54
|
-
|
|
840
|
+
In the HTML version, Preferences opens the switches inside the bar:
|
|
55
841
|
|
|
56
|
-

|
|
57
843
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
844
|
+
A headless UI owns its focus handling and labels. Test it with a keyboard and
|
|
845
|
+
a screen reader. The headless pages for [React](https://c15t.com/docs/frameworks/react/headless),
|
|
846
|
+
[Vue](https://c15t.com/docs/frameworks/vue/headless) and [Svelte](https://c15t.com/docs/frameworks/svelte/headless)
|
|
847
|
+
cover notices and custom preference forms.
|
|
61
848
|
|
|
62
|
-
## Check
|
|
849
|
+
## Check a design
|
|
63
850
|
|
|
64
|
-
|
|
851
|
+
Run these checks after you adopt a design:
|
|
65
852
|
|
|
66
|
-
|
|
67
|
-
|
|
853
|
+
1. Clear site data and reload. The banner appears and no optional vendor
|
|
854
|
+
request appears in DevTools Network.
|
|
855
|
+
2. Click Reject all, reload, and confirm the banner stays closed and the
|
|
856
|
+
vendors stay blocked.
|
|
857
|
+
3. Open preferences from your footer link, allow one category, save, and
|
|
858
|
+
confirm only that category's vendors load.
|
|
859
|
+
4. Test a 375-pixel viewport with your longest translation, and tab through
|
|
860
|
+
every button.
|
|
68
861
|
|
|
69
|
-
|
|
862
|
+
[Verify consent](../guides/verify-consent.md) has the full list.
|
|
70
863
|
|
|
71
|
-
|
|
72
|
-
`Docs / Customization`: Brand Card, Brand Bar and Choice Wall. Each demo uses
|
|
73
|
-
in-memory consent and no analytics; Reset example returns it to a fresh choice.
|
|
74
|
-
The screenshots above come from these v3 examples.
|
|
864
|
+
## Run the designs
|
|
75
865
|
|
|
76
|
-
The
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
866
|
+
The source repository renders each design in its Storybook under
|
|
867
|
+
`Docs / Banner designs`: `apps/storybook-react`, `apps/storybook-vue`,
|
|
868
|
+
`apps/storybook-svelte` and `apps/storybook-astro`. Each story uses an
|
|
869
|
+
in-memory policy and loads no analytics. The HTML headless bar runs at
|
|
870
|
+
`/consent-example/headless` in `examples/script-tag`, which the example
|
|
871
|
+
acceptance tests cover.
|