@c15t/react 3.0.0-alpha.3 → 3.0.0-alpha.5
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 +92 -72
- package/README.md +21 -17
- package/SKILL.md +34 -0
- 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/components/consent-gate/atoms.js +1 -1
- package/dist/components/consent-gate/consent-gate.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/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/root.js +1 -1
- package/dist/components/panel-trigger/atoms/button.js +1 -1
- package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
- package/dist/components/preferences/atoms/accordion.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/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/dialog/dialog.js +1 -1
- package/dist/components/shared/ui/icon.js +1 -1
- package/dist/context/consent-tracking-context.js +1 -1
- package/dist/devtools.js +1 -1
- package/dist/draft-context.js +2 -0
- package/dist/draft.js +1 -1
- package/dist/external-iab-context.js +1 -1
- package/dist/gpp.js +2 -0
- package/dist/hooks/use-color-scheme.js +1 -1
- package/dist/hooks/use-focus-trap.js +1 -1
- package/dist/hooks.js +1 -1
- package/dist/iab/styles.tw3.css +4 -4
- package/dist/index-parts/consent-banner.js +1 -0
- package/dist/index-parts/consent-dialog-link.js +1 -0
- package/dist/index-parts/consent-dialog-trigger.js +1 -0
- package/dist/index-parts/consent-dialog.js +1 -0
- package/dist/index-parts/consent-gate.js +1 -0
- package/dist/index-parts/core.js +1 -0
- package/dist/index-parts/draft.js +1 -0
- package/dist/index-parts/hooks.js +1 -0
- package/dist/index-parts/module-hooks.js +1 -0
- package/dist/index-parts/provider.js +1 -0
- package/dist/index-parts/theme.js +1 -0
- package/dist/index-parts/translations.js +1 -0
- package/dist/index.js +1 -1
- package/dist/kernel-selector.js +1 -1
- package/dist/module-hooks/iframe-blocker.js +1 -1
- package/dist/postcss-tailwind3.js +1 -0
- 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 -16
- package/dist/transports/offline.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/component-hooks/use-headless-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/iab-panel.d.ts +2 -2
- package/dist-types/components/iab-panel/index.d.ts +5 -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/prompt/components.d.ts +6 -0
- package/dist-types/components/shared/libs/compose-refs.d.ts +20 -0
- package/dist-types/components/shared/ui/icon.d.ts +2 -1
- 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-context.d.ts +31 -0
- package/dist-types/draft.d.ts +13 -30
- package/dist-types/gpp.d.ts +27 -0
- package/dist-types/hooks/use-focus-trap.d.ts +3 -1
- package/dist-types/hooks.d.ts +57 -13
- package/dist-types/iab.d.ts +1 -1
- package/dist-types/index-parts/consent-banner.d.ts +7 -0
- package/dist-types/index-parts/consent-dialog-link.d.ts +7 -0
- package/dist-types/index-parts/consent-dialog-trigger.d.ts +7 -0
- package/dist-types/index-parts/consent-dialog.d.ts +7 -0
- package/dist-types/index-parts/consent-gate.d.ts +7 -0
- package/dist-types/index-parts/core.d.ts +8 -0
- package/dist-types/index-parts/draft.d.ts +7 -0
- package/dist-types/index-parts/hooks.d.ts +7 -0
- package/dist-types/index-parts/module-hooks.d.ts +7 -0
- package/dist-types/index-parts/provider.d.ts +7 -0
- package/dist-types/index-parts/theme.d.ts +8 -0
- package/dist-types/index-parts/translations.d.ts +7 -0
- package/dist-types/index.d.ts +14 -15
- package/dist-types/kernel-selector.d.ts +0 -8
- package/dist-types/module-hooks/iframe-blocker.d.ts +12 -7
- package/dist-types/postcss-tailwind3.d.ts +21 -0
- package/dist-types/provider.d.ts +57 -17
- 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 +7 -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 +92 -72
- 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 +22 -7
- package/docs/{guides → concepts}/consent-state.md +89 -105
- 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 -34
- package/docs/customization/recipes.md +839 -49
- 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 +163 -96
- package/docs/customization/translations.md +60 -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 +56 -50
- package/docs/frameworks/react/components/consent-gate.md +37 -20
- package/docs/frameworks/react/components/consent-provider.md +130 -0
- package/docs/frameworks/react/components/consent-widget.md +23 -20
- package/docs/frameworks/react/components/dev-tools.md +53 -85
- 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/gpp.md +163 -0
- package/docs/frameworks/react/{hooks/overview.md → hooks.md} +21 -16
- package/docs/frameworks/react/{iab/overview.md → iab.md} +80 -31
- package/docs/frameworks/react/network-blocker.md +35 -41
- 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 +167 -159
- package/docs/integrations/ahrefs-analytics.md +152 -154
- package/docs/integrations/amplitude.md +162 -156
- package/docs/integrations/building-integrations.md +136 -37
- package/docs/integrations/clearbit.md +154 -154
- package/docs/integrations/cloudflare-web-analytics.md +156 -156
- package/docs/integrations/cloudflare-zaraz.md +209 -261
- package/docs/integrations/crisp.md +164 -158
- package/docs/integrations/databuddy.md +157 -173
- package/docs/integrations/fathom-analytics.md +158 -156
- package/docs/integrations/front-chat.md +167 -167
- package/docs/integrations/google-maps.md +118 -83
- package/docs/integrations/google-tag-manager.md +178 -163
- package/docs/integrations/google-tag.md +163 -160
- package/docs/integrations/heap.md +163 -155
- package/docs/integrations/hightouch.md +161 -157
- package/docs/integrations/hotjar.md +159 -155
- package/docs/integrations/intercom.md +183 -153
- package/docs/integrations/klaviyo.md +486 -0
- package/docs/integrations/linkedin-insights.md +174 -150
- package/docs/integrations/logrocket.md +160 -156
- package/docs/integrations/matomo-analytics.md +188 -178
- package/docs/integrations/meta-pixel.md +188 -150
- package/docs/integrations/microsoft-clarity.md +163 -155
- package/docs/integrations/microsoft-uet.md +148 -154
- package/docs/integrations/mixpanel-analytics.md +155 -160
- package/docs/integrations/one-dollar-stats.md +166 -165
- package/docs/integrations/openai-pixel.md +204 -301
- package/docs/integrations/overview.md +137 -80
- package/docs/integrations/pinterest-tag.md +191 -183
- package/docs/integrations/pirsch.md +169 -159
- package/docs/integrations/plausible-analytics.md +172 -158
- package/docs/integrations/posthog.md +313 -240
- package/docs/integrations/promptwatch.md +154 -154
- package/docs/integrations/reddit-pixel.md +185 -157
- package/docs/integrations/rudderstack.md +201 -186
- package/docs/integrations/rybbit-analytics.md +171 -160
- package/docs/integrations/segment.md +182 -154
- package/docs/integrations/snapchat-pixel.md +186 -156
- package/docs/integrations/tiktok-pixel.md +171 -150
- package/docs/integrations/umami-analytics.md +163 -158
- package/docs/integrations/vercel-analytics.md +166 -157
- package/docs/integrations/x-pixel.md +176 -150
- package/docs/integrations/youtube.md +121 -86
- package/docs/upgrade-v3.md +496 -467
- package/package.json +20 -9
- package/dist/ui-save.js +0 -1
- package/dist-types/ui-save.d.ts +0 -25
- 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 -95
- package/docs/frameworks/react/script-loader.md +0 -69
- package/docs/frameworks/react/styling/overview.md +0 -266
- package/docs/guides/deployment-modes.md +0 -75
- package/docs/guides/shared-consent-controls.md +0 -158
- package/docs/integrations/clear-on-revocation.md +0 -167
- package/docs/integrations/granular-consent.md +0 -210
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: How consent works
|
|
3
|
+
description: What c15t decides on each page load, the difference between a
|
|
4
|
+
permission and a recorded choice, and what happens when a visitor saves.
|
|
5
|
+
group: concepts
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## What c15t decides on each page load
|
|
9
|
+
|
|
10
|
+
On every page load c15t answers one question for each consent category: is
|
|
11
|
+
this allowed right now? To answer it, c15t:
|
|
12
|
+
|
|
13
|
+
1. Selects a **policy** for the visitor, usually by country and region. The
|
|
14
|
+
policy says whether optional categories start denied (opt-in) or allowed
|
|
15
|
+
(opt-out), and whether to show a banner.
|
|
16
|
+
2. Reads the visitor's **stored choice** from a cookie and localStorage, if
|
|
17
|
+
there is one that still matches the policy.
|
|
18
|
+
3. Reads **privacy signals** such as Global Privacy Control (GPC).
|
|
19
|
+
|
|
20
|
+
The result is a set of **permissions**, one per category. Components, script
|
|
21
|
+
loaders, embeds and your own code read those permissions. Nothing optional
|
|
22
|
+
runs until the policy has resolved: while it loads, and if it fails, every
|
|
23
|
+
optional category is denied.
|
|
24
|
+
|
|
25
|
+
A banner is only the interface. It does not stop a script you load with a
|
|
26
|
+
plain `<script>` tag or a vendor SDK you initialize yourself. To gate that code,
|
|
27
|
+
register it with c15t. See [integrations](../integrations/overview.md).
|
|
28
|
+
|
|
29
|
+
## Categories
|
|
30
|
+
|
|
31
|
+
| Category | Use it for |
|
|
32
|
+
| --------------- | -------------------------------------------------- |
|
|
33
|
+
| `necessary` | Code the site cannot work without. Always allowed. |
|
|
34
|
+
| `functionality` | Optional features such as support chat. |
|
|
35
|
+
| `measurement` | Analytics and usage measurement. |
|
|
36
|
+
| `experience` | Personalization. |
|
|
37
|
+
| `marketing` | Advertising, retargeting and pixels. |
|
|
38
|
+
|
|
39
|
+
Assign a category by what the code does, not by what would be convenient.
|
|
40
|
+
Calling analytics `necessary` does not make it necessary.
|
|
41
|
+
[Consent categories](./consent-categories.md) explains which
|
|
42
|
+
categories the preference dialog shows and how policy scope affects them.
|
|
43
|
+
|
|
44
|
+
## Policies
|
|
45
|
+
|
|
46
|
+
A policy has a **model** and a **prompt**:
|
|
47
|
+
|
|
48
|
+
| Model | Optional categories before a choice |
|
|
49
|
+
| --------- | ----------------------------------------------------------------- |
|
|
50
|
+
| `opt-in` | Denied until the visitor allows them. |
|
|
51
|
+
| `opt-out` | Allowed until the visitor refuses them or sends a privacy signal. |
|
|
52
|
+
| `iab` | Controlled by an IAB TCF consent string. |
|
|
53
|
+
| `none` | Allowed, with no prompt. |
|
|
54
|
+
|
|
55
|
+
| Prompt | What the visitor sees |
|
|
56
|
+
| -------- | ------------------------------------------------------------------------------ |
|
|
57
|
+
| `choice` | A banner asking for a decision. |
|
|
58
|
+
| `notice` | A notice they can dismiss. Dismissing records an acknowledgement, not consent. |
|
|
59
|
+
| `none` | Nothing on load. The policy can still require a way to open preferences. |
|
|
60
|
+
|
|
61
|
+
With [Inth](https://inth.com) you manage policies in your project, and the app
|
|
62
|
+
receives them from the backend. Changing a stylesheet or importing a preset in
|
|
63
|
+
the browser does not override a hosted policy.
|
|
64
|
+
[Policies](./policies.md) covers presets, scope, and why a banner may
|
|
65
|
+
not appear.
|
|
66
|
+
|
|
67
|
+
## A permission is not a recorded choice
|
|
68
|
+
|
|
69
|
+
These two values answer different questions, and mixing them up is the most
|
|
70
|
+
common integration bug:
|
|
71
|
+
|
|
72
|
+
* **Permission** (`effectivePermissions`, `useConsent('measurement')`) answers
|
|
73
|
+
"may this run now?" Under an opt-out policy it can be `true` before the
|
|
74
|
+
visitor has done anything.
|
|
75
|
+
* **Recorded choice** (`explicitChoice`) answers "what did the visitor decide?"
|
|
76
|
+
It only changes when the visitor accepts, rejects or saves preferences.
|
|
77
|
+
|
|
78
|
+
| Task | Read |
|
|
79
|
+
| ---------------------------------------------------- | ------------------- |
|
|
80
|
+
| Load a script or render an optional feature | Permission |
|
|
81
|
+
| Show what the visitor chose, or report consent rates | Recorded choice |
|
|
82
|
+
| Decide whether a prompt is needed | `promptRequirement` |
|
|
83
|
+
| Find out why a region behaves differently | `policyRule` |
|
|
84
|
+
| Check that the policy loaded | `resolution.status` |
|
|
85
|
+
|
|
86
|
+
Never turn a permission into a saved choice. Hydrating server state, reading a
|
|
87
|
+
cookie or rendering a component does not count as a visitor action.
|
|
88
|
+
`onChoiceRecorded` fires only for a visitor's action; `onPermissionsChanged`
|
|
89
|
+
also fires when a choice expires or a privacy signal changes.
|
|
90
|
+
|
|
91
|
+
## Notices and privacy signals
|
|
92
|
+
|
|
93
|
+
Dismissing a notice acknowledges it. It grants nothing and leaves earlier
|
|
94
|
+
refusals in place.
|
|
95
|
+
|
|
96
|
+
GPC is a browser setting that asks sites not to sell or share data. c15t reads
|
|
97
|
+
it live on every evaluation and applies the restrictions your policy configures
|
|
98
|
+
for it. It never becomes a stored refusal: when the browser stops sending GPC,
|
|
99
|
+
the restriction ends. The backend also sees the current value.
|
|
100
|
+
|
|
101
|
+
## What happens when a visitor saves
|
|
102
|
+
|
|
103
|
+
When a visitor clicks Accept, Reject or Save, c15t:
|
|
104
|
+
|
|
105
|
+
1. Updates permissions and closes the banner or dialog in the same click.
|
|
106
|
+
Scripts, embeds and network rules follow the new permissions immediately.
|
|
107
|
+
2. Writes the choice to the cookie and localStorage.
|
|
108
|
+
3. Sends the choice to the consent backend. If the request fails, the choice
|
|
109
|
+
stays saved in the browser and the request is retried later.
|
|
110
|
+
4. Reloads the page if the visitor turned off something they had allowed.
|
|
111
|
+
Code that already ran cannot be unloaded, so the reload starts a page with
|
|
112
|
+
only permitted code. Set `reloadOnConsentRevoked: false` to handle revocation
|
|
113
|
+
yourself.
|
|
114
|
+
|
|
115
|
+
The [consent state reference](./consent-state.md) documents the exact
|
|
116
|
+
ordering, retries, server hydration and how open tabs stay in step.
|
|
117
|
+
|
|
118
|
+
## Where consent data comes from
|
|
119
|
+
|
|
120
|
+
A consent backend supplies policies and stores consent records. Use Inth, run
|
|
121
|
+
the [c15t backend](https://c15t.com/docs/self-host/overview) yourself, or keep policies in the
|
|
122
|
+
browser for local development. [Choose your setup](./choose-your-setup.md)
|
|
123
|
+
walks through the options for your framework and hosting.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
3
|
-
description:
|
|
4
|
-
|
|
5
|
-
group:
|
|
2
|
+
title: Policies
|
|
3
|
+
description: How policy models, prompts and scope decide what c15t asks
|
|
4
|
+
visitors, where to change the rules, and why a banner may not appear.
|
|
5
|
+
group: concepts
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## What the policy controls
|
|
@@ -29,12 +29,12 @@ must do the same.
|
|
|
29
29
|
Use `effectivePermissions` to gate a script or embed. A `true` value under an
|
|
30
30
|
opt-out rule does not mean the visitor clicked Accept. Read `explicitChoice`
|
|
31
31
|
when you need the visitor's recorded decision. See
|
|
32
|
-
[consent
|
|
32
|
+
[how consent works](./how-consent-works.md#a-permission-is-not-a-recorded-choice) for these distinctions.
|
|
33
33
|
|
|
34
34
|
## Where to change the rules
|
|
35
35
|
|
|
36
36
|
For Inth, configure policy rules on your hosted project. The app receives those
|
|
37
|
-
rules through the configured [data-fetching path](
|
|
37
|
+
rules through the configured [data-fetching path](./data-fetching.md). Importing
|
|
38
38
|
`recommendedPolicyRules()` or changing browser styling does not override the
|
|
39
39
|
hosted policy.
|
|
40
40
|
|
|
@@ -42,7 +42,7 @@ For a backend you operate, use the
|
|
|
42
42
|
[policy packs guide](https://c15t.com/docs/self-host/guides/policy-packs). It covers
|
|
43
43
|
`manifest.policyRules`, preset selection and custom matchers. Browser-only
|
|
44
44
|
setups own their rules locally; see
|
|
45
|
-
[
|
|
45
|
+
[choose your setup](./choose-your-setup.md).
|
|
46
46
|
|
|
47
47
|
Presets are starting configurations. Select them for your actual processing,
|
|
48
48
|
and review their assumptions before allowing optional categories by default.
|
|
@@ -69,55 +69,3 @@ default for other known locations. An unknown country uses its strict opt-in
|
|
|
69
69
|
fallback. A US country with a missing state gets the US opt-out fallback.
|
|
70
70
|
These are the local pack's defaults, not a promise about your Inth project.
|
|
71
71
|
Browser-only mode does not discover a visitor's country from their IP address.
|
|
72
|
-
|
|
73
|
-
## Inspect the active policy
|
|
74
|
-
|
|
75
|
-
Place this diagnostic component inside your existing consent
|
|
76
|
-
provider. It reads state without recording a choice.
|
|
77
|
-
|
|
78
|
-
```tsx title="src/consent-policy-debug.tsx"
|
|
79
|
-
import { useSnapshot } from 'c15t/react';
|
|
80
|
-
|
|
81
|
-
export function ConsentPolicyDebug() {
|
|
82
|
-
const snapshot = useSnapshot();
|
|
83
|
-
const resolution = snapshot.resolution;
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<pre>
|
|
87
|
-
{JSON.stringify(
|
|
88
|
-
{
|
|
89
|
-
status: resolution.status,
|
|
90
|
-
policy: resolution.status === 'matched' ? resolution.policy.id : null,
|
|
91
|
-
prompt: snapshot.promptRequirement,
|
|
92
|
-
permissions: snapshot.effectivePermissions,
|
|
93
|
-
location: snapshot.location,
|
|
94
|
-
privacySignals: snapshot.privacySignals,
|
|
95
|
-
},
|
|
96
|
-
null,
|
|
97
|
-
2,
|
|
98
|
-
)}
|
|
99
|
-
</pre>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
The diagnostic subscribes to consent changes. Remove it after verification.
|
|
105
|
-
For a production feature gate, use `useConsent('marketing')` or the category
|
|
106
|
-
your feature needs instead of subscribing to the whole snapshot.
|
|
107
|
-
|
|
108
|
-
## Change appearance without changing policy
|
|
109
|
-
|
|
110
|
-
Keep one banner and one dialog in your existing
|
|
111
|
-
provider. Use
|
|
112
|
-
[styling](../styling/overview.md) to change their appearance,
|
|
113
|
-
or [headless UI](../headless.md) for custom markup.
|
|
114
|
-
Presentation settings change layout and emphasis; policy rules decide which
|
|
115
|
-
actions and privacy rights the app must expose.
|
|
116
|
-
|
|
117
|
-
## Verify policy behavior
|
|
118
|
-
|
|
119
|
-
Use a fresh browser profile to check the intended location, then repeat with a
|
|
120
|
-
saved rejection and GPC enabled. Confirm that the expected prompt and
|
|
121
|
-
preferences controls appear, and that denied scripts and embeds make no
|
|
122
|
-
requests. Test missing location inputs too. Follow
|
|
123
|
-
[verify consent](../../../guides/verify-consent.md) for the full flow.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Class names and CSS-in-JS
|
|
3
|
+
description: Style c15t's component parts with CSS Modules, vanilla-extract,
|
|
4
|
+
StyleX, Emotion or plain class names, and see which approach works in each
|
|
5
|
+
framework.
|
|
6
|
+
group: customization
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Pass a class to a part
|
|
10
|
+
|
|
11
|
+
CSS Modules, vanilla-extract, StyleX, Emotion and Tailwind all end up as a class
|
|
12
|
+
on an element and a rule in a stylesheet. Give c15t the class through your
|
|
13
|
+
framework's part API, and the rule wins over c15t's own rule for that part:
|
|
14
|
+
c15t's rules sit in the `components` cascade layer, and an unlayered rule from
|
|
15
|
+
your stylesheet outranks any layered one.
|
|
16
|
+
|
|
17
|
+
Each example below adds a 3px colored border and 4px corners to the banner card.
|
|
18
|
+
The React examples run as Storybook stories in CI, which check that each class
|
|
19
|
+
lands on the card and that the computed border comes from the class, not from
|
|
20
|
+
c15t's own card rule. The script tag examples run in the example app's browser
|
|
21
|
+
tests.
|
|
22
|
+
|
|
23
|
+
| Framework | Part API | Class key | Inline `style` | Where the class's CSS must load |
|
|
24
|
+
| ------------------------------ | ----------------------------------------------------------------------- | ----------------------------------------- | -------------- | ------------------------------------------------------------------------------------ |
|
|
25
|
+
| Next.js, TanStack Start, React | `components.<component>.<part>`, or `theme.slots` | `className` | Yes | Anywhere on the page |
|
|
26
|
+
| Nuxt, Vue | `components.<component>.<part>`, or `theme.slots` | `class` (`className` in `theme.slots`) | Yes | Anywhere on the page |
|
|
27
|
+
| Svelte, SvelteKit | `theme.slots`, or `class` on a component | A string, or `className` in a slot object | Yes | A global stylesheet, or `:global()` |
|
|
28
|
+
| Astro | `theme.slots` in the integration options, or `class` on `ConsentBanner` | A string, or `className` in a slot object | Yes | A global stylesheet |
|
|
29
|
+
| HTML, JavaScript | `ui.theme.slots` | A string, or `className` in a slot object | Yes | Inside the shadow root through `ui.stylesheetURLs`, or anywhere with `shadow: false` |
|
|
30
|
+
|
|
31
|
+
[Component parts](./slots.md) lists the parts and their keys in
|
|
32
|
+
each framework.
|
|
33
|
+
|
|
34
|
+
## CSS Modules
|
|
35
|
+
|
|
36
|
+
Import the module and pass its class. This works wherever your bundler compiles
|
|
37
|
+
CSS Modules:
|
|
38
|
+
|
|
39
|
+
```css title="src/banner.module.css"
|
|
40
|
+
.card {
|
|
41
|
+
border: 3px solid rgb(219 39 119);
|
|
42
|
+
border-radius: 4px;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```ts title="src/consent-components.ts"
|
|
47
|
+
import type { ConsentProviderOptions } from 'c15t/react';
|
|
48
|
+
|
|
49
|
+
import styles from './banner.module.css';
|
|
50
|
+
|
|
51
|
+
/** Pass as `components` in your ConsentProvider options. */
|
|
52
|
+
export const components: ConsentProviderOptions['components'] = {
|
|
53
|
+
banner: { card: { className: styles.card } },
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Pass `components` in your `ConsentProvider` options, or in `options` on
|
|
58
|
+
`ConsentRoot` in Next.js and TanStack Start.
|
|
59
|
+
|
|
60
|
+
## vanilla-extract
|
|
61
|
+
|
|
62
|
+
vanilla-extract compiles `style()` calls in `.css.ts` files to static CSS at
|
|
63
|
+
build time. Add its plugin for your bundler, such as
|
|
64
|
+
`@vanilla-extract/vite-plugin`, then pass the class:
|
|
65
|
+
|
|
66
|
+
```ts title="src/consent-components.css.ts"
|
|
67
|
+
import { style } from '@vanilla-extract/css';
|
|
68
|
+
import type { ConsentProviderOptions } from 'c15t/react';
|
|
69
|
+
|
|
70
|
+
const card = style({ border: '3px solid rgb(37 99 235)', borderRadius: 4 });
|
|
71
|
+
|
|
72
|
+
/** Pass as `components` in your ConsentProvider options. */
|
|
73
|
+
export const components: ConsentProviderOptions['components'] = {
|
|
74
|
+
banner: { card: { className: card } },
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## StyleX
|
|
79
|
+
|
|
80
|
+
`stylex.props()` returns `className` and, for dynamic values, `style`. A React
|
|
81
|
+
part accepts both, so spread the result into the part. Add the StyleX compiler
|
|
82
|
+
plugin for your bundler, such as `@stylexjs/unplugin`:
|
|
83
|
+
|
|
84
|
+
```ts title="src/consent-components.ts"
|
|
85
|
+
import * as stylex from '@stylexjs/stylex';
|
|
86
|
+
import type { ConsentProviderOptions } from 'c15t/react';
|
|
87
|
+
|
|
88
|
+
const styles = stylex.create({
|
|
89
|
+
card: {
|
|
90
|
+
borderColor: 'rgb(22 163 74)',
|
|
91
|
+
borderRadius: 4,
|
|
92
|
+
borderStyle: 'solid',
|
|
93
|
+
borderWidth: 3,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Pass as `components` in your ConsentProvider options. `stylex.props()`
|
|
99
|
+
* returns `className` and, for dynamic styles, `style`; the slot takes both.
|
|
100
|
+
*/
|
|
101
|
+
export const components: ConsentProviderOptions['components'] = {
|
|
102
|
+
banner: { card: stylex.props(styles.card) },
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
StyleX writes atomic classes into an unlayered stylesheet, so they outrank
|
|
107
|
+
c15t's layered rules without extra specificity.
|
|
108
|
+
|
|
109
|
+
## Emotion
|
|
110
|
+
|
|
111
|
+
`css()` from `@emotion/css` returns a class name and inserts its rule into a
|
|
112
|
+
`<style>` element in the document `<head>` when your code runs:
|
|
113
|
+
|
|
114
|
+
```ts title="src/consent-components.ts"
|
|
115
|
+
import { css } from '@emotion/css';
|
|
116
|
+
import type { ConsentProviderOptions } from 'c15t/react';
|
|
117
|
+
|
|
118
|
+
const card = css({ border: '3px solid rgb(234 88 12)', borderRadius: 4 });
|
|
119
|
+
|
|
120
|
+
/** Pass as `components` in your ConsentProvider options. */
|
|
121
|
+
export const components: ConsentProviderOptions['components'] = {
|
|
122
|
+
banner: { card: { className: card } },
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Emotion inserts its rules into the page, not into a shadow root. With the HTML
|
|
127
|
+
script tag or `@c15t/browser`, set `ui: { shadow: false }` so the UI renders
|
|
128
|
+
in the page where those rules reach it. `ui.stylesheetURLs` cannot carry them,
|
|
129
|
+
because they have no URL.
|
|
130
|
+
|
|
131
|
+
## Use other frameworks' part APIs
|
|
132
|
+
|
|
133
|
+
Vue and Nuxt bind a part's object to the element with `v-bind`, so write
|
|
134
|
+
`class` rather than `className`. A class string from CSS Modules,
|
|
135
|
+
vanilla-extract or Emotion works the same way. To use StyleX, map its
|
|
136
|
+
`className` to `class`.
|
|
137
|
+
|
|
138
|
+
Svelte scopes component styles, so a class defined in a component's `<style>`
|
|
139
|
+
block does not reach a c15t part. Define it in a global stylesheet or with
|
|
140
|
+
`:global(.your-class)`. `theme.slots` accepts a string or
|
|
141
|
+
`{ className, style }`, and `class` on `ConsentBanner` goes on the banner root.
|
|
142
|
+
|
|
143
|
+
Astro serializes its integration options, so `theme.slots` in
|
|
144
|
+
`astro.config.mjs` takes plain strings and objects. Use class names from a
|
|
145
|
+
global stylesheet or Tailwind there. Build-time tools that export class names
|
|
146
|
+
from a module, such as CSS Modules, cannot reach `astro.config.mjs`.
|
|
147
|
+
|
|
148
|
+
## Style the script tag's shadow root
|
|
149
|
+
|
|
150
|
+
The script tag and `init()` from `@c15t/browser` render into a shadow root. A
|
|
151
|
+
class from your page's stylesheet reaches a part only if the stylesheet reaches
|
|
152
|
+
the shadow root. Pick one:
|
|
153
|
+
|
|
154
|
+
* **`ui.stylesheetURLs`.** c15t links each URL inside the shadow root, after
|
|
155
|
+
its own stylesheet. Link the stylesheet that holds your classes, such as your
|
|
156
|
+
CSS Modules or Tailwind build.
|
|
157
|
+
* **`::part()`.** Every part with a slot key carries it in a `part` attribute.
|
|
158
|
+
Page CSS can style it with `[data-c15t-ui]::part(consentBannerCard)`,
|
|
159
|
+
without any class.
|
|
160
|
+
* **`ui.css`.** A string of CSS that c15t adds inside the shadow root.
|
|
161
|
+
* **`shadow: false`.** c15t renders into the page and every page stylesheet
|
|
162
|
+
applies, including Emotion's.
|
|
163
|
+
|
|
164
|
+
This example links a Tailwind build into the shadow root and puts utilities on
|
|
165
|
+
two parts:
|
|
166
|
+
|
|
167
|
+
```html title="index.html"
|
|
168
|
+
<link
|
|
169
|
+
rel="stylesheet"
|
|
170
|
+
href="/tailwind.css"
|
|
171
|
+
/>
|
|
172
|
+
<script>
|
|
173
|
+
window.c15t = window.c15t || [];
|
|
174
|
+
c15t.push([
|
|
175
|
+
'config',
|
|
176
|
+
{
|
|
177
|
+
ui: {
|
|
178
|
+
// The banner renders in a shadow root, where the page's
|
|
179
|
+
// stylesheets do not reach. Link your Tailwind build into it
|
|
180
|
+
// too, and keep the page's own link: Tailwind 4 registers
|
|
181
|
+
// variables with @property, which only works in the page.
|
|
182
|
+
stylesheetURLs: ['/tailwind.css'],
|
|
183
|
+
theme: {
|
|
184
|
+
slots: {
|
|
185
|
+
consentBannerCard: 'rounded-none border-4 border-sky-600',
|
|
186
|
+
consentBannerTitle: 'uppercase tracking-wide',
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
</script>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Check the result
|
|
196
|
+
|
|
197
|
+
1. Open the page in a private window so the banner shows.
|
|
198
|
+
2. Select the banner card in the Elements panel. It carries your class.
|
|
199
|
+
3. In the Styles panel, your rule applies and c15t's rule for the same
|
|
200
|
+
property is struck out. If your class is on the element but its rule is
|
|
201
|
+
missing, the stylesheet does not reach the part. Check the "Where the class's CSS
|
|
202
|
+
must load" column in [Pass a class to a part](#pass-a-class-to-a-part).
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Dark mode
|
|
3
|
+
description: Switch c15t's banner and dialog to dark colors with colorScheme,
|
|
4
|
+
set your own dark tokens, follow your site's theme switch, and paint dark on
|
|
5
|
+
the first frame in every framework.
|
|
6
|
+
group: customization
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## How c15t turns dark
|
|
10
|
+
|
|
11
|
+
c15t's stylesheet switches every `--c15t-*` color token to its dark value when
|
|
12
|
+
`<html>` has a `dark` or a `c15t-dark` class. The `colorScheme` option decides
|
|
13
|
+
whether c15t sets `c15t-dark` itself:
|
|
14
|
+
|
|
15
|
+
| `colorScheme` | What c15t does with `c15t-dark` |
|
|
16
|
+
| ------------- | ------------------------------------------------------------------------ |
|
|
17
|
+
| `'light'` | Removes it once |
|
|
18
|
+
| `'dark'` | Adds it once |
|
|
19
|
+
| `'system'` | Follows `prefers-color-scheme`, including changes while the page is open |
|
|
20
|
+
| Unset | Copies your `dark` class into `c15t-dark` and follows it as it changes |
|
|
21
|
+
| `null` | Leaves it alone. Your site sets it |
|
|
22
|
+
|
|
23
|
+
Astro also accepts `'none'`, which means the same as `null`.
|
|
24
|
+
|
|
25
|
+
`'light'` only removes `c15t-dark`. If your site puts `dark` on `<html>`, the
|
|
26
|
+
stylesheet still switches the tokens to dark. Pick one class convention for
|
|
27
|
+
your site and let c15t follow it.
|
|
28
|
+
|
|
29
|
+
## Defaults in each framework
|
|
30
|
+
|
|
31
|
+
| Framework | Where to set it | Default |
|
|
32
|
+
| ----------------------- | --------------------------------------------------------------------------------- | -------------------- |
|
|
33
|
+
| Next.js, TanStack Start | `options.colorScheme` on `ConsentRoot`, and `colorScheme` on `ConsentTheme` | Unset: copies `dark` |
|
|
34
|
+
| React | `colorScheme` in the `ConsentProvider` options, and on `ConsentTheme` | Unset: copies `dark` |
|
|
35
|
+
| Nuxt | `colorScheme` under the `c15t` key in `nuxt.config.ts`, or in `app/app.config.ts` | Unset: copies `dark` |
|
|
36
|
+
| Vue | `colorScheme` in the `c15tVue` options | Unset: copies `dark` |
|
|
37
|
+
| Astro | `colorScheme` in the `c15t()` integration options | `'system'` |
|
|
38
|
+
| Svelte, SvelteKit | `colorScheme` on `ConsentManagerProvider` | Unset: copies `dark` |
|
|
39
|
+
| HTML | `data-color-scheme` on the script tag: `light`, `dark`, `system` or `none` | `system` |
|
|
40
|
+
| JavaScript | `ui.colorScheme` in `init()` | `'system'` |
|
|
41
|
+
|
|
42
|
+
React, Vue and Svelte apps usually already have a theme switch that toggles a
|
|
43
|
+
`dark` class, such as next-themes. Copying that class keeps c15t in step with
|
|
44
|
+
the site without extra code.
|
|
45
|
+
|
|
46
|
+
Astro and the script tag default to `'system'` because neither can rely on a
|
|
47
|
+
site convention. Astro paints the banner from server HTML before any site
|
|
48
|
+
script runs, and Astro sites share no `dark` class convention, so following the
|
|
49
|
+
operating system is the one choice its inline script can make correctly. A plain
|
|
50
|
+
HTML page has no convention either. A site that has one opts in with `null`.
|
|
51
|
+
|
|
52
|
+
### Set colorScheme null in Nuxt
|
|
53
|
+
|
|
54
|
+
In Nuxt, set `colorScheme: null` under the `c15t` key in `nuxt.config.ts` or
|
|
55
|
+
in `app/app.config.ts`. Nuxt drops a `null` in inline module options, such as
|
|
56
|
+
`modules: [['@c15t/vue', { colorScheme: null }]]`, before the module reads it,
|
|
57
|
+
so c15t would copy your `dark` class as if `colorScheme` were unset.
|
|
58
|
+
|
|
59
|
+
## Follow your site's theme switch
|
|
60
|
+
|
|
61
|
+
In React, Next.js, TanStack Start, Vue, Nuxt, Svelte and SvelteKit, leave
|
|
62
|
+
`colorScheme` unset and toggle `dark` on `<html>`. c15t watches the class and switches with
|
|
63
|
+
it. Include the class in the server HTML when your site renders dark, so the
|
|
64
|
+
first paint matches.
|
|
65
|
+
|
|
66
|
+
In Astro, set `colorScheme: 'none'` and toggle `c15t-dark` together with your
|
|
67
|
+
own class. A `ClientRouter` navigation replaces the attributes of `<html>`, so
|
|
68
|
+
set the class again on `astro:after-swap` if your theme script does not.
|
|
69
|
+
|
|
70
|
+
With the script tag, add `data-color-scheme="none"`. With `init()`, pass
|
|
71
|
+
`ui: { colorScheme: null }`. The UI is then dark while `<html>` has `dark` or
|
|
72
|
+
`c15t-dark`, and follows the class as it changes. The UI renders in a shadow
|
|
73
|
+
root that the page's class cannot reach, so c15t copies the class onto the
|
|
74
|
+
shadow host.
|
|
75
|
+
|
|
76
|
+
If your site sets `c15t-dark` itself in React, Next.js, TanStack Start, Vue,
|
|
77
|
+
Nuxt, Svelte or SvelteKit, pass `colorScheme: null` so c15t leaves it alone.
|
|
78
|
+
|
|
79
|
+
## Set your own dark colors
|
|
80
|
+
|
|
81
|
+
Theme tokens take a `dark` object with the same color keys as `colors`. Every
|
|
82
|
+
color you set in `colors` also applies in dark mode, unless `dark` sets it too,
|
|
83
|
+
so give a dark value for each brand color:
|
|
84
|
+
|
|
85
|
+
```ts title="consent-theme.ts"
|
|
86
|
+
export const theme = {
|
|
87
|
+
colors: { primary: '#2f6f4e', primaryHover: '#24563c', surface: '#fbf8f3' },
|
|
88
|
+
dark: { primary: '#7fd1a8', primaryHover: '#9fdcbd', surface: '#1b1f1d' },
|
|
89
|
+
};
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Where the theme goes depends on the framework:
|
|
93
|
+
|
|
94
|
+
* **React, Next.js, TanStack Start.** Pass it to `ConsentTheme`. To write the
|
|
95
|
+
CSS outside a component, call `generateThemeCSS(theme, colorScheme)` from
|
|
96
|
+
`c15t/react/utils`, or `@c15t/react/utils` if you installed the scoped
|
|
97
|
+
package. It writes the same CSS as `ConsentTheme`.
|
|
98
|
+
* **Vue, Nuxt.** Pass it as `theme` in the plugin or module options. It goes
|
|
99
|
+
into the `<style id="c15t-css-vars">` element with `tokens`.
|
|
100
|
+
* **Astro.** Pass it as `theme` in the integration options.
|
|
101
|
+
* **SvelteKit, Svelte.** Pass it to `generateThemeCSS(theme, colorScheme)` from
|
|
102
|
+
`@c15t/ui/theme` on the server.
|
|
103
|
+
* **HTML, JavaScript.** Pass it as `ui.theme`.
|
|
104
|
+
|
|
105
|
+
This Nuxt example follows the system setting and uses its own dark primary
|
|
106
|
+
color. Every other dark token keeps c15t's default:
|
|
107
|
+
|
|
108
|
+
```ts title="nuxt.config.ts (c15t options)"
|
|
109
|
+
// Follow the visitor's system setting, with a dark primary of our own.
|
|
110
|
+
colorScheme: 'system',
|
|
111
|
+
theme: { dark: { primary: '#7fd1a8' } },
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
To set dark values in plain CSS instead, write them for both classes:
|
|
115
|
+
|
|
116
|
+
```css
|
|
117
|
+
:root.dark,
|
|
118
|
+
:root.c15t-dark {
|
|
119
|
+
--c15t-primary: #7fd1a8;
|
|
120
|
+
--c15t-surface: #1b1f1d;
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A generated theme, such as the output of `ConsentTheme` or
|
|
125
|
+
`generateThemeCSS`, writes its dark values on `:root:root.dark` and
|
|
126
|
+
`:root:root.c15t-dark`. Those selectors outrank the rule above. If you use both,
|
|
127
|
+
put dark values in the theme, or repeat `:root` in your own selectors.
|
|
128
|
+
|
|
129
|
+
## Paint dark on the first frame
|
|
130
|
+
|
|
131
|
+
A banner that renders light and then turns dark flashes. How to avoid that
|
|
132
|
+
depends on where the banner first renders:
|
|
133
|
+
|
|
134
|
+
| Framework | What to do |
|
|
135
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
136
|
+
| Next.js, TanStack Start | Render `<ConsentTheme theme={theme} colorScheme="system" />` on the server, with the same value as `options.colorScheme`. `'dark'` writes dark tokens as the default and `'system'` adds a `prefers-color-scheme` media query, so the server HTML is dark before hydration. With `colorScheme` unset, put your `dark` class in the server HTML. |
|
|
137
|
+
| React | Render `<ConsentTheme colorScheme="system" />` next to the provider, with the same value as the provider's `colorScheme`. Its CSS makes the tokens dark from the first frame, before the provider sets the class. In a server-rendered React app, render it on the server. |
|
|
138
|
+
| Nuxt | For `'dark'` and `'system'`, the module adds an inline script to `<head>` that sets `c15t-dark` before the server-rendered banner paints. It carries the module's `nonce`. With `colorScheme` unset, put your `dark` class in the server HTML. |
|
|
139
|
+
| Vue | The plugin applies `colorScheme` and writes the tokens when you install it, before the first render. |
|
|
140
|
+
| Astro | `ConsentScript` in `<head>` sets the class from an inline script before the banner paints, and c15t sets it again after each `ClientRouter` navigation. Keep `ConsentScript` in your layout's `<head>`. |
|
|
141
|
+
| Svelte | The provider sets the class as it mounts. With `colorScheme` unset, the stylesheet reads your `dark` class directly, so the tokens are dark from the first frame. |
|
|
142
|
+
| SvelteKit | Pass the scheme to `generateThemeCSS(theme, 'system')` in your server load, so the CSS in `<svelte:head>` already has the media query. With `colorScheme` unset, put your `dark` class in the server HTML. |
|
|
143
|
+
| HTML, JavaScript | c15t applies the scheme when it mounts the UI, before any surface renders. |
|
|
144
|
+
|
|
145
|
+
[Theme tokens](./tokens.md) lists every color token, and your
|
|
146
|
+
framework's customize page shows where its theme goes.
|
|
147
|
+
|
|
148
|
+
## Check the result
|
|
149
|
+
|
|
150
|
+
1. Open the page in a private window with your operating system in dark mode,
|
|
151
|
+
or add your `dark` class to `<html>`.
|
|
152
|
+
2. The banner is dark on its first frame. Reload with the Network panel
|
|
153
|
+
throttled to see the first paint.
|
|
154
|
+
3. Open the preference dialog. It uses the same dark tokens as the banner.
|
|
155
|
+
4. Switch the scheme while the page is open. With `'system'` or an unset
|
|
156
|
+
`colorScheme`, the banner and dialog follow without a reload.
|
|
157
|
+
5. Check the contrast of the primary button text in both schemes.
|