Documentation
Native CMP
Everything you need to install, configure, translate, style and extend the consent manager. Looking for ready-made service setups? See the examples.
Overview
Native CMP is a consent manager for cookies, third-party scripts and embeds, built from regular Webstudio building blocks. There is no external service, dashboard or vendor script: everything lives in your project and can be edited in the Builder.
- Engine: a small script in Project Settings → Custom Code. It reads the stored decision before the page paints, blocks and activates third-party code, deletes cookies and keeps everything in sync during client-side navigation.
- Consent Manager Slot: the notice, preferences modal, purposes and switches as native instances, shared across all pages.
- Variables:
consentServicesdescribes what each service does technically;consentTranslationsholds all copy per language. - Attributes: behavior is wired with
data-cmp-*attributes, so any element you design can open the settings, show a status or wait for consent. - Craft tokens: every part of the UI is a composite Token backed by semantic CSS variables.
Native CMP is free and open source under the AGPL-3.0-or-later license. The source code, build scripts and tests are on GitHub.
The CMP gives you technical building blocks for a GDPR- and ePrivacy-friendly setup: prior blocking, granular purposes, equally prominent choices, revocation and cookie cleanup. Whether a site is compliant also depends on its texts, services and privacy policy.
Installation
Install with an AI agent (recommended)
Connect Claude Code, Codex, Cursor or any agent to your project with the Webstudio MCP (npx webstudio@latest link with a Builder share link, then npx webstudio@latest connect) and ask it to follow nativecmp.com/install.txt. The agent:
- scans Custom Code, HTML Embeds and YouTube or Vimeo components and recognizes 23 common services,
- installs the engine, design tokens and the Consent Manager Slot on every page,
- configures each service with its cookies and descriptions in your site’s languages,
- blocks scripts and wraps embeds and videos in Consent Gates,
- adds a section per service to your privacy policy, for you to review,
- maps the consent UI to your colors, radii and fonts and verifies the result.
It shows you its plan before changing anything and never publishes. See Install with AI for the prompt. Prefer to do it yourself? Use the generator, copy from the examples or follow the manual steps below.
Starting a new site? Clone the Native CMP Starter, a Webstudio project with the engine, the Consent Manager, a Consent preview page and example gates already set up. See it live at starter.nativecmp.com.
Manual installation
1. Add the engine
Open Project Settings → Custom Code and paste the CMP snippet at the very top, before any analytics or tag manager code. It contains three parts:
Custom Code
<!-- Native CMP: configuration -->
<script>
window.cmpConfig = {
storageName: "cmp_consent",
cookieExpiresAfterDays: 365,
};
</script>
<style data-cmp-critical>/* critical CSS */</style>
<script data-cmp-engine="1.1.0">/* engine */</script>
<!-- Consent-managed head scripts go below this line -->2. Add the Consent Manager Slot
Copy the Consent Manager Slot to every page. On new projects, add it to your page template so new pages include it automatically. The Slot is shared: editing it on one page updates every page.
3. Configure services and copy
Select the Consent Manager root instance and edit the consentServices and consentTranslations variables in the Data panel. The examples contain ready-made entries for the most common services.
4. Block third-party code
Change existing scripts and embeds so they only run with consent (see Blocking scripts & embeds) and add a Privacy settings link to your footer with data-cmp-action="open-modal".
5. Publish and test
Open the site in a private window. The notice must appear, nothing tracked should load before a decision, and your choice must survive reloads and page changes. cmp.getConsents() in the browser console shows the current state.
How it works
- Before the first paint: the engine runs in
<head>, reads thecmp_consentcookie and sets state attributes on<html>. The critical CSS uses them to show or hide the notice immediately, so returning visitors never see a flash. - After the page is parsed: the engine reads the service definitions from the Slot, validates the stored decision, activates allowed scripts and embeds, and renders switches, gates and status indicators.
- During client-side navigation: Webstudio remounts page content on every route change. A
MutationObservernotices new consent-related elements and re-syncs them. Consent state lives in the engine, not in the page, so it survives navigation. - No double execution: React may re-create embed content (for example right after hydration). The engine remembers what it has already executed per embed container, so scripts never run twice.
Effective consent
A service is active when it is required, when the visitor accepted it once in a gate, or when it has consent and the decision is confirmed. Services marked optOut also run before a decision, until the visitor declines them.
Changed services
If the service list changes after a visitor decided, the stored decision becomes unconfirmed: the notice opens again with the “services changed” message, and non-required services stay blocked until the visitor decides again.
Configuration
Set options in window.cmpConfig at the top of the Custom Code snippet. All options are optional.
| Option | Default | Description |
|---|---|---|
storage | "cookie" | Where the decision is stored: "cookie" or "localStorage". |
storageName | "cmp_consent" | Cookie or localStorage key. |
cookieExpiresAfterDays | 365 | Lifetime of the consent cookie. |
cookieDomain | "" | Set e.g. ".example.com" to share consent across subdomains. |
cookiePath | "/" | Path of the consent cookie. |
default | false | Preselection for services without their own default. |
mustConsent | false | Open the preferences modal instead of the notice. It can’t be closed until the visitor decides. |
noNotice | false | Contextual consent only: no notice on page load. Consent Gates, Privacy settings links and the JavaScript API ask for consent. See Contextual consent only. |
confirmDelay | 800 | Milliseconds the modal stays visible after Accept all / Decline all, so visitors see the switches change. |
consentMode | null | Google Consent Mode v2 mapping, see Google Consent Mode & GTM. |
consentModeDefaults | null | Extra fields for gtag("consent", "default", …), e.g. { security_storage: "granted" }. |
dataLayer | false | Push { event: "cmp_consent", cmp_type, cmp_consents } to window.dataLayer on every decision. |
fallbackLanguage | "" | Language shown when no translation matches the page. Defaults to the first entry. |
debug | false | Log service definitions and cookie deletions to the console. |
Contextual consent only
Set noNotice: true if you only want to protect embeds such as YouTube or Google Maps and don’t need a notice on page load. The generator calls this Contextual consent only. Visitors allow a provider right where its content appears: Load content allows it for the current page, Always allow stores the decision straight away. The Privacy settings link still opens the full preferences.
In this mode the root gets data-cmp-mode="contextual", and Always allow buttons are visible before any decision. Head scripts such as analytics stay blocked until visitors enable them in the preferences, so use it for sites without consent-based tracking. mustConsent takes precedence if both are set.
Stored value
The cookie contains URL-encoded JSON: { "consents": { "google-analytics": true, … }, "timestamp": "2026-09-13T10:00:00.000Z", "version": 1 }. The timestamp documents when the decision was made.
Services
consentServices is a JSON array on the Consent Manager root. It is rendered as a hidden Service Registry that the engine reads.
consentServices
[
{ "name": "consent-manager", "required": true },
{ "name": "google-analytics", "cookies": ["^_ga", "_gid", "^_gat"] },
{ "name": "youtube" },
{ "name": "chat", "optOut": true, "dependsOn": ["google-analytics"] }
]| Field | Type | Description |
|---|---|---|
name | string | Unique key. Referenced by data-cmp-service, data-cmp-gate, data-cmp-status and translations. Use lowercase and dashes. |
required | boolean | Always active. The switch is disabled and the “Always active” badge is shown. |
default | boolean | Preselected in the preferences modal before the visitor decides. |
optOut | boolean | Runs before a decision, until the visitor declines. Shows “Active by default”. |
cookies | string[] | Cookies deleted when consent is missing or revoked. Patterns starting with ^ are regular expressions (^_ga matches _ga_ABC123); other values match exactly. No spaces inside a pattern. |
onlyOnce | boolean | Accept scripts run at most once per page load, even across client-side navigation. |
contextualOnly | boolean | Ignored by Accept all / Decline all. Only its switch or a Consent Gate changes it. |
dependsOn | string[] | Enabling this service enables the listed services; disabling one of them disables this service. |
Cookie deletion works for first-party cookies the browser lets JavaScript see: the current host, the host with a leading dot and every parent domain, on path / and the current path. HttpOnly cookies and cookies on third-party domains can’t be removed from the page.
Languages & translations
consentTranslations contains one entry per language. The Slot renders one notice and modal per entry and the engine shows the one matching the page language from Page Settings → Language (rendered as <html lang>).
consentTranslations
[
{
"lang": "en",
"dir": "ltr",
"privacyLabel": "privacy policy",
"privacyUrl": "/privacy",
"notice": {
"title": "We value your privacy",
"text": "We use cookies … Learn more in our",
"changes": "Our services have changed since your last visit.",
"customize": "Customize", "decline": "Decline all", "accept": "Accept all"
},
"modal": {
"title": "Privacy settings", "text": "Choose which services we may use …",
"close": "Close privacy settings", "toggleAll": "Enable or disable all services",
"alwaysActive": "Always active", "activeByDefault": "Active by default",
"service": "service", "services": "services",
"decline": "Decline all", "save": "Save selection", "accept": "Accept all"
},
"purposes": [
{
"id": "analytics", "title": "Analytics", "description": "…",
"services": [
{ "name": "google-analytics", "title": "Google Analytics", "description": "…" }
]
}
]
}
]Language matching
- Exact match, case-insensitive:
de-AT→de-at. - Primary language:
de-AT→de, or a regional entry of the same language. cmpConfig.fallbackLanguage, otherwise the first entry.
The language updates live when client-side navigation changes <html lang>. Consent is shared across languages. Purposes group services in the modal; a purpose switch toggles all its non-required services and shows a mixed state when only some are active.
Any language
- Add an entry with any language code:
fr,pt-BR,tr,fa, …. The engine doesn’t need a list of supported languages. - The generator includes built-in notice, modal, purpose and gate texts and descriptions for all 23 catalog services in English, German, Spanish, French, Italian, Portuguese, Dutch, Polish, Arabic and Hebrew.
- For other codes the generator starts from English and marks every field English – translate in its translation editor. The editor also imports translations from pasted configurations.
Right-to-left languages
Each entry may set "dir": "rtl" or "ltr". Without it, the Consent Manager detects right-to-left from the language code: ar, arc, ckb, dv, fa, he, ku, ps, sd, ug, ur and yi. The language wrapper gets the matching dir attribute, so the notice, modal and switches mirror automatically.
- Layouts use flex and grid, which follow the writing direction.
- Tokens that need explicit mirroring use logical properties or the
:dir(rtl)state. For example,consent-switchmirrors its thumb andconsent-modal-closeusesmargin-inline-end. - Set
dir="rtl"on the Body of right-to-left pages too, so your own page content mirrors as well. See the Arabic demo.
Other translated elements
Add data-cmp-lang="de" to any element, for example a footer link in a shared Slot, to show it only on German pages.
Blocking scripts & embeds
Mark third-party code with data-cmp-service. Put it in Custom Code (runs once per visit) or an HTML Embed (runs on the pages that contain it).
External scripts
HTML
<script type="text/plain" data-cmp-service="google-analytics"
data-src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"></script>type="text/plain" stops the browser from running the script. On consent the engine inserts a real copy into <head> with all other attributes (id, data-*, nonce, async). Use data-type to restore a type such as module. The same external URL is only loaded once per page load.
Inline scripts and decline handlers
HTML
<script type="text/plain" data-cmp-service="google-analytics">
gtag("config", "G-XXXX");
</script>
<script type="text/plain" data-cmp-service="google-analytics" data-cmp-on="decline">
window["ga-disable-G-XXXX"] = true;
</script>Inline scripts run when consent is granted, again after a revoke followed by a new consent, and once for every page that renders them. data-cmp-on="decline" runs a script when the service is denied or revoked.
Templates for iframes and widgets
HTML Embed
<template data-cmp-service="youtube">
<iframe src="https://www.youtube-nocookie.com/embed/VIDEO_ID" title="Video"></iframe>
</template>Everything inside a <template> is inert. On consent the content is inserted after the template (scripts go to <head>); on revoke it is removed again. This is the most robust option for iframes, widgets and social embeds.
Attribute swapping
HTML
<iframe data-cmp-service="vimeo" data-src="https://player.vimeo.com/video/ID"></iframe>
<img data-cmp-service="tracker" data-src="https://example.com/pixel.gif" alt="">
<link rel="stylesheet" data-cmp-service="google-fonts" data-href="https://fonts.googleapis.com/…">For other elements the engine copies data-src, data-srcset, data-href, data-poster and data-data into the real attributes on consent and removes them on revoke (iframes switch to about:blank). Iframes, images, video, audio, embed and object elements stay hidden until granted. Stylesheet links are added to and removed from <head>.
For YouTube and Vimeo, use Webstudio’s native components inside an interaction-mode Consent Gate (see Consent Gates). Turn off Show preview, Autoplay and Preconnect first: with those settings on, the components contact YouTube or Vimeo as soon as the page loads.
Element state
Every managed element gets data-cmp-state="granted" or "denied", which you can use for styling.
Consent Gates
A Consent Gate lets visitors allow a single service right where its content appears. Gates have two modes.
Placeholder mode (default)
The content stays hidden behind a designed placeholder until the visitor allows the service. Use it for HTML embeds such as maps, social posts and widgets.
Structure
<div data-cmp-gate="google-maps"> <!-- consent-gate -->
<div data-cmp-gate-notice> <!-- visible until consent -->
<p>Google Maps</p>
<button data-cmp-action="accept-once">Load content</button>
<button data-cmp-action="accept-always" data-cmp-if="confirmed">Always allow Google Maps</button>
<button data-cmp-action="open-modal">Privacy settings</button>
</div>
<div data-cmp-gate-content> <!-- HTML Embed with a template -->
<template data-cmp-service="google-maps">…</template>
</div>
</div>Interaction mode
The content stays visible and works like a normal component until someone interacts with it. The first click inside data-cmp-gate-content is held back and the notice appears on top. After Load content or Always allow, the original click continues automatically, so a video starts playing right away.
Structure
<div data-cmp-gate="youtube" data-cmp-gate-mode="interaction"> <!-- consent-gate -->
<div data-cmp-gate-notice> <!-- consent-gate-notice + is-consent-gate-overlay -->
<p>YouTube video</p>
<button data-cmp-action="accept-once">Load content</button>
<button data-cmp-action="accept-always" data-cmp-if="confirmed">Always allow YouTube</button>
<button data-cmp-action="dismiss">Cancel</button>
<button data-cmp-action="open-modal">Privacy settings</button>
</div>
YouTube component with data-cmp-gate-content <!-- consent-video -->
</div>- Focus moves into the notice when it opens. Cancel (
dismiss) or Escape closes it and returns focus to the element that was clicked. - While the notice is open, the gate has
data-cmp-requestedand grows to at least 300 px (400 px on phones), so the notice never gets cut off. - Visitors who already allowed the service interact with the content directly.
- If consent is revoked while a native player is loaded, the engine stops it (
about:blank). It can be played again after the next page load. - Interaction mode works for anything that only loads on click, for example your own Load map button.
Webstudio’s YouTube and Vimeo components
Put the component inside an interaction-mode gate, add data-cmp-gate-content to it in Settings → Attributes, and use these settings:
| Setting | YouTube | Vimeo | Why |
|---|---|---|---|
| Show preview | Off | Off | On loads the thumbnail from img.youtube.com, or calls vimeo.com/api and loads from vimeocdn.com. |
| Autoplay | Off | Off | On loads the player immediately. |
| Preconnect | Off | – | On opens a connection to YouTube on page load. |
| Privacy enhanced mode | On | – | Uses youtube-nocookie.com once the player loads. |
| Do not track | – | On | Asks Vimeo not to track the session. |
| Preview Image | Your own upload | Your own upload | Self-hosted thumbnails need no consent. |
The generator copies ready-made YouTube and Vimeo gates with these settings and your video URL. The tests confirm that no request reaches YouTube, Vimeo or their image servers before consent.
Accept once and always
- Load content (
accept-once) grants consent for the current page view only; nothing is stored. - Always allow (
accept-always) stores consent for this service. It is only offered after the visitor made a first decision, so a single click in a gate never counts as a decision about all other services. - Buttons outside a gate can target a service with
data-cmp-name="youtube".
Attribute reference
Actions
| Value of data-cmp-action | Effect |
|---|---|
accept-all | Grant all services (except contextualOnly), save and close. |
decline-all | Deny all non-required services, save and close. |
save | Save the current switch positions and close. |
open-modal | Open the preferences modal. Works on any element on any page. |
close | Close the modal. Unsaved switch changes are discarded for returning visitors. Ignored when mustConsent requires a decision. |
accept-once | Allow the service of the closest gate for this page view. |
accept-always | Allow and store the service of the closest gate. |
dismiss | Close an interaction-mode gate notice without deciding. |
reset | Delete the stored decision and show the notice again. |
Structure and bindings
| Attribute | Used on | Purpose |
|---|---|---|
data-cmp-notice | Notice region | Shown while the notice is open. |
data-cmp-modal | Modal wrapper | Shown while the preferences modal is open. |
data-cmp-dialog | Dialog element | Receives focus; Tab is trapped inside. |
data-cmp-toggle | Checkbox | service:<name>, purpose:<id> or all. |
data-cmp-purpose | Purpose row | Groups service rows for purpose switches and badges. |
data-cmp-service-item | Service row | Connects a translated row to a service. |
data-cmp-service-def | Registry item | Technical service definition (rendered from consentServices). |
data-cmp-service | Script, template, link, iframe… | Blocked until the service is granted. |
data-cmp-on | Script | accept (default) or decline. |
data-cmp-gate | Gate wrapper | Service name of a Consent Gate. |
data-cmp-gate-mode | Gate wrapper | interaction: keep the content visible and hold back the first click until consent. |
data-cmp-gate-notice / data-cmp-gate-content | Gate children | Notice and gated content. |
data-cmp-requested | Gate wrapper (set by the engine) | An interaction-mode notice is open. |
data-cmp-status | Any element | Live status of a service; sets data-cmp-state. |
data-cmp-lang | Any element | Only shown when it matches the page language. |
data-cmp-preview | Design frame | notice or modal: shows the fixed UI inline for designing. |
Conditional visibility
| data-cmp-if | Visible when |
|---|---|
confirmed / unconfirmed | The visitor has (not) made a decision. |
changed | The service list changed since the stored decision. |
required / not-required | The closest service or purpose is (not) required. |
opt-out | The closest service is opt-out. |
granted / denied | The closest service, status or gate is (not) active. |
State on <html>
| Attribute | Values |
|---|---|
data-cmp | loading until the page is parsed, then ready. |
data-cmp-open | notice or modal while one is open. |
data-cmp-confirmed | true or false. |
data-cmp-changed | Present when services changed. |
data-cmp-confirming | Present during the confirm delay. |
data-cmp-language | Resolved language, e.g. de. |
JavaScript API & events
JavaScript
cmp.show(); // open the preferences modal
cmp.showNotice(); cmp.hide();
cmp.getConsent("youtube"); // effective consent: true / false
cmp.getConsents(); // { "google-analytics": true, … }
cmp.isConfirmed(); // visitor has decided
cmp.getConsentId(); // "c7f3aa11-…", the ID stored with the decision
cmp.getServices(); // parsed service definitions
cmp.getLanguage(); // "en"
cmp.setConsent("youtube", true); // change and save (pass false as 3rd argument to skip saving)
cmp.acceptAll(); cmp.declineAll(); cmp.save(); cmp.reset();
cmp.refresh(); // re-sync after custom DOM changes
const off = cmp.on("save", ({ type, consents, changes }) => {});
document.addEventListener("cmp:service", (event) => console.log(event.detail));| Event | Detail | Fires when |
|---|---|---|
ready | consents | Service definitions were read after the first page load. |
change | consents | A switch changed (not yet saved). |
save | { type, consents, changes } | A decision was stored. type is accept, decline, save, contextual-accept or api. |
service | { name, consent, service } | A service’s effective consent changed. |
init | { service } | A service was seen for the first time. |
modal | { open } | The preferences modal opened or closed. |
gate | { name, requested } | An interaction-mode gate held back a click and opened its notice. |
Every event is also dispatched on document as cmp:<event>.
Proof of consent
The GDPR (Art. 7(1)) asks you to be able to demonstrate that a visitor consented. Native CMP stores every decision in the visitor’s browser, in the cmp_consent cookie, together with a random consent ID. The preferences dialog shows that ID, so visitors can quote it when they ask what they agreed to.
Proof has two parts, and neither of them needs an IP address:
- What someone chose: the consent ID links a person to their entries. IP addresses change with every network, are shared by whole households and are personal data themselves, so they prove nothing here and are never stored.
- That you respected the choice: this follows from how your site behaves, not from a log. Keep your test results and a record of which services and texts each version of your banner contained. Supervisory authorities check this by loading the site and watching what happens before a decision.
Optional: your own consent log
For a record you control, deploy the consent log to your own Cloudflare account: a small Worker with a database, from the same repository as Native CMP. Then set its URL in your Custom Code. Everything stays in your account, and ELECOS never sees the data.
Custom Code
window.cmpConfig = {
consentLog: "https://consentlog.example.com",
};| Stored per entry | Example | Why |
|---|---|---|
| Consent ID | c7f3aa11-… | Links the entry to the visitor’s cookie, without identifying them. |
| Time of the decision | 2026-09-20T10:15:31Z | When consent was given or withdrawn. |
| Type | accept, decline, save, reset | How the decision was made. |
| Choices | {"google-analytics": false} | What exactly was allowed. |
| Config fingerprint | 1k3f9x | Which service list and texts the visitor saw. |
| Language | de | Which translation was shown. |
| Site (origin) | https://example.com | Added by the log itself, not sent by the browser. |
Never stored: IP address, page URL, user agent or anything else about the visitor. Entries are sent with navigator.sendBeacon, so a decision is never delayed and a blocked request never breaks the page.
- Deploy: open the deploy page, sign in to Cloudflare and confirm. The Worker and its database are created for you, and you set the export token during setup.
- Use your own domain: Add a custom domain such as
consentlog.yourdomain.comin the Cloudflare dashboard, so the log is not a third-party domain for your visitors. - Retention: entries are deleted after
RETENTION_DAYS, 3 years by default. Change it in the Cloudflare dashboard under Settings → Variables. - Restrict writers: set
ALLOWED_ORIGINSto your own site, so only your pages can add entries. - Export: open the log URL in a browser and download the CSV, for all entries or for one consent ID.
- Other backends: any endpoint that accepts a POST works, for example an n8n or Make webhook. The Worker source shows the payload.
In your privacy policy, name the log, for example: “When you make a privacy choice, we store that choice together with a random ID, the time and the language on our own server (Cloudflare) for three years, so that we can demonstrate your consent. Your IP address is not stored.”
Live example: this site runs its own log at consentlog.nativecmp.com, deployed from log/ in the repository. Decisions made on nativecmp.com are recorded there, without IP addresses.
A consent log is evidence, not compliance. Whether your site is lawful also depends on your texts, your services and your privacy policy.
Google Consent Mode & GTM
Map Google consent types to your services. The engine sends gtag("consent", "default") with all mapped types denied as soon as it runs, and gtag("consent", "update") whenever effective consent changes. A type is granted when any of its services is active.
Custom Code
window.cmpConfig = {
consentMode: {
analytics_storage: ["google-analytics"],
ad_storage: ["google-ads"],
ad_user_data: ["google-ads"],
ad_personalization: ["google-ads"],
},
consentModeDefaults: { security_storage: "granted" },
dataLayer: true,
};- Place Google Tag Manager or gtag below the CMP snippet, so the default command is queued first.
- With
dataLayer: true, GTM receives acmp_consentevent withcmp_consentsfor custom triggers. - To block GTM completely until consent, mark its script like any other service instead.
Design & Craft tokens
Every consent token reads a namespaced set of **--cmp-* variables** on Global Root. Restyle the whole consent UI there, or edit a single token in the Style panel. Each variable defaults to the matching Craft variable when your project has one (e.g. --cmp-accent: var(--background-accent, #1d4ed8)), so Craft projects match automatically. In any other project, point them at your own variables.
Theme variables
| Variable | Default | Used for |
|---|---|---|
--cmp-background | var(--background-primary, #ffffff) | Notice, dialog and gate background |
--cmp-surface | var(--background-secondary, #f3f4f6) | Purpose and service rows, gate placeholder, button hover base |
--cmp-text | var(--foreground-primary, #15171c) | Text |
--cmp-text-muted | var(--foreground-secondary, #4d5461) | Descriptions and secondary text |
--cmp-border | var(--border-default, #dde0e5) | Dividers and badges |
--cmp-control | var(--border-control, #6f7582) | Switch track when off, outlined button border |
--cmp-accent | var(--background-accent, #1d4ed8) | Primary button, switch when on, links |
--cmp-on-accent | var(--foreground-on-accent, #ffffff) | Text on the primary button |
--cmp-focus | var(--border-focus, var(--cmp-accent)) | Focus ring |
--cmp-warning | var(--foreground-warning, #9a4a00) | “Services changed” note |
--cmp-disabled | var(--foreground-disabled, var(--cmp-text-muted)) | Disabled text |
--cmp-inverse | var(--background-inverse, var(--cmp-text)) | Video placeholder background |
--cmp-scrim | var(--overlay-scrim, color-mix(in srgb, var(--cmp-text) 55%, transparent)) | Backdrop behind the dialog |
--cmp-hover | var(--overlay-interaction-hover, color-mix(in srgb, var(--cmp-text) 6%, transparent)) | Hover tint on buttons |
--cmp-pressed | var(--overlay-interaction-pressed, color-mix(in srgb, var(--cmp-text) 12%, transparent)) | Pressed tint on buttons |
--cmp-on-accent-hover | var(--overlay-on-accent-hover, color-mix(in srgb, var(--cmp-on-accent) 14%, transparent)) | Hover tint on the primary button |
--cmp-on-accent-pressed | var(--overlay-on-accent-pressed, color-mix(in srgb, var(--cmp-on-accent) 24%, transparent)) | Pressed tint on the primary button |
--cmp-radius | var(--radius-default, 16px) | Notice, dialog and gate corners |
--cmp-radius-small | var(--radius-small, 10px) | Small corners (rows, placeholders) |
--cmp-radius-full | var(--radius-full, 999px) | Switches, badges and the close button |
--cmp-button-radius | var(--cmp-radius-full) | Button corners (set 0 for square buttons, switches keep --cmp-radius-full) |
--cmp-button-border-width | 1px | Button border width |
--cmp-shadow | var(--shadow-overlay, 0 24px 64px -12px rgb(0 0 0 / 0.28)) | Notice and dialog shadow |
--cmp-heading-font | inherit | Font of titles and purpose/service labels (inherit = page font) |
--cmp-heading-weight | 600 | Weight of titles and labels |
--cmp-heading-transform | none | Letter case of titles and labels, e.g. uppercase |
--cmp-heading-letter-spacing | normal | Letter spacing of titles and labels |
--cmp-button-font | inherit | Button font |
--cmp-button-weight | 600 | Button weight |
--cmp-button-transform | none | Button letter case |
--cmp-button-letter-spacing | normal | Button letter spacing |
--cmp-gap-xs | var(--gap-xs, 4px) | Spacing |
--cmp-gap-s | var(--gap-s, 8px) | Spacing |
--cmp-gap-m | var(--gap-m, 16px) | Spacing |
--cmp-gap-l | var(--gap-l, 24px) | Spacing |
--cmp-spacing | var(--spacing-default, 24px) | Inner padding of notice and dialog |
--cmp-focus-width | var(--focus-width, 2px) | Focus ring width |
--cmp-focus-offset | var(--focus-offset, 2px) | Focus ring offset |
--cmp-duration | var(--duration-default, 180ms) | Transition duration |
--cmp-easing | var(--easing-default, cubic-bezier(0.2, 0, 0, 1)) | Transition easing |
--cmp-layer | var(--layer-overlay, 2147483000) | z-index of notice and dialog |
Tokens are named consent-* and variant tokens is-consent-* (Craft modifiers, always combined with their base token). When you restyle tokens directly, include both prefixes: the button borders and colors live in is-consent-button-primary and is-consent-button-secondary.
Composite tokens
| Block | Elements and variants |
|---|---|
consent-notice | consent-notice-body, consent-notice-title, consent-notice-text, consent-notice-changes, consent-notice-actions |
consent-modal | consent-modal-scrim, consent-modal-dialog, consent-modal-header, consent-modal-title, consent-modal-close, consent-modal-text, consent-modal-body, consent-modal-footer |
consent-purposes | consent-purpose, is-consent-purpose-all, consent-purpose-header, consent-purpose-label, consent-purpose-description, consent-purpose-services, consent-purpose-summary |
consent-services | consent-service, consent-service-header, consent-service-label, consent-service-description |
consent-button | is-consent-button-primary, is-consent-button-secondary, is-consent-button-ghost |
consent-switch | States: :checked, :indeterminate, :disabled, :focus-visible, ::before (thumb) |
consent-gate | consent-gate-notice, consent-gate-title, consent-gate-text, consent-gate-actions, consent-gate-content |
consent-status | consent-status-label, is-consent-status-label-granted |
| Others | consent, consent-language, consent-link, consent-badge, consent-preview, is-consent-preview-modal |
Designing in the Builder
Custom Code doesn’t run in the Builder, so the engine is missing there: the fixed notice and preferences dialog stay hidden and only the first language is shown. To edit them, select the Consent Manager root, open the Data panel and set the Builder preview variables:
| Variable | Values | Effect in the Builder |
|---|---|---|
canvasPreview | off, notice, modal, hide | Shows the notice or the preferences dialog on the canvas, on every page with the Consent Manager Slot. With off, a small hint in the corner of the canvas points to this variable; hide removes the hint. |
canvasLanguage | empty or a language code, e.g. de | Shows that translation instead of the first one, including data-cmp-lang labels elsewhere on the page. |
Select elements on the canvas or in the Navigator and edit texts in consentTranslations, styles in the consent-* tokens. The flags only act while the engine is absent, so the published site is unaffected even if you forget to switch them back to off. Gates always show both their notice and content in the Builder.
Accessibility
- Default color pairs meet WCAG 2.2 AA: body text ≥ 6.8:1, accent buttons 7.1:1, switch tracks ≥ 3.7:1 against their backgrounds.
- The modal is a
role="dialog"witharia-modal, labelled by its title and described by its text. Focus moves into it, Tab is trapped, Escape closes it and focus returns to the opener. - Switches are native checkboxes with
role="switch"; purpose switches expose a mixed state. Every switch has a visible label and a description. - Required and opt-out states are communicated with text badges, not color alone. Focus rings use
--border-focus. - The notice is a labelled region that doesn’t steal focus, so keyboard and screen reader users can keep reading the page.
Automated audits may report that role="switch" needs aria-checked. For native checkboxes the checked state is exposed by the browser, and adding a static aria-checked would be wrong.
Troubleshooting
| Symptom | Check |
|---|---|
| The notice never appears | Is the CMP snippet in Custom Code and is the Consent Manager Slot on the page? Was a decision already stored? Run cmp.reset(). |
| A script doesn’t run after consent | Does data-cmp-service exactly match a name in consentServices? Is the original type text/plain? Check document.head for a script with data-cmp-for. |
| A script runs before consent | The original tag must not have a JavaScript type, and the URL must be in data-src, not src. |
| An embed appears twice or stays empty | Use a <template> inside an HTML Embed rather than an iframe with data-src next to other markup. |
| Tracking cookies remain after declining | Compare cookie names with the cookies patterns. Cookies on other domains or HttpOnly cookies can’t be deleted by JavaScript. |
| Wrong language | Check Page Settings → Language and cmp.getLanguage(); add a matching entry to consentTranslations. |
| A native video contacts YouTube or Vimeo before consent | Turn off Show preview, Autoplay and Preconnect on the component, and check that it has data-cmp-gate-content inside a gate with data-cmp-gate-mode="interaction". |
| A right-to-left language shows left-to-right | Add "dir": "rtl" to its entry, or use a language code from the right-to-left list. |
| The notice reopens for returning visitors | The service list changed. This is intentional: new services need a new decision. |
| Want to debug | Set debug: true in cmpConfig and watch the console. |
Migrating from other consent managers
- Remove the old consent manager’s script and its styles from Custom Code.
- Recreate your services in
consentServicesand your copy inconsentTranslations. - Replace attributes on blocked elements:
data-name="x"→data-cmp-service="x".type="text/plain",data-src,data-hrefanddata-typework as before. - Replace
show()calls withcmp.show()and callbacks withcmp.on("service", …). - Use a new
storageNameif the old manager used the same cookie name, so visitors are asked once with the new UI.