UI Widget — Advanced Configuration
new ConsentiSetup(config) accepts a single ConsentiConfig object. All top-level keys are optional — the widget works with an empty config object, auto-detecting the compliance group from the browser. New here? Start with the Configuration quick start instead — this page is the complete reference, every field with its default value.
Full configuration reference
Every available option shown with its default value.
import { ConsentiSetup } from '@consenti/ui'
const widget = new ConsentiSetup({
// ── Compliance group (optional) ──────────────────────────────────────────────
compliance: {
type: 'opt-in', // see Compliance groups table below
geoDataProvider: undefined, // custom function to resolve visitor country
ageGate: { // see "UI Widget — Age Gate" guide
enabled: false,
minimumAge: 13, // COPPA = 13; GDPR Article 8 = 16
requireParentalConsent: false,
},
tcf: { // see "TCF v2.2 Implementation Guide"; cmpId/cmpVersion
enabled: false, // must match the backend's tcf config
cmpId: 0,
cmpVersion: 1,
},
},
// ── Core behaviour (optional) ───────────────────────────────────────────────
core: {
tenantId: 'my-site', // logical identifier for this installation
locale: 'en', // BCP 47; falls back to language prefix, then 'en'
dir: 'auto', // 'ltr' | 'rtl' | 'auto' — 'auto' derives from locale
autoHonorGPC: true, // false | true | 'strict'
storage: 'cookie', // 'cookie' | 'localStorage'
cookieDomains: '.example.com',
allowReceipt: true,
disableCssTemplate: false,
userId: 'server-assigned-uuid', // authenticated users only
usePrebuiltProfiles: true, // load pre-built profiles instead of constructing from config
cacheResolvedProfiles: true, // cache resolved profile in sessionStorage (1h TTL)
console: ['error', 'warn'], // log levels to emit; 'error' | 'warn' | 'info' | 'debug'
theme: {
bgColor: '#ffffff',
textColor: '#1a1a1a',
primaryColor: '#1565c0',
primaryTextColor: '#ffffff',
secondaryColor: '#f0f4f8',
secondaryTextColor: '#1a3460',
borderColor: '#e2e8f0',
accentColor: '#d32f2f',
accentTextColor: '#ffffff',
fontFamily: 'system-ui, sans-serif',
fontSizeBase: '14px',
fontSizeHeading: '18px',
fontSizeMultiplier: '1',
borderRadius: '8px',
buttonBorderRadius: '4px',
toggleBgOn: '#1565c0',
toggleBgOff: '#cccccc',
},
},
// ── Mount point (optional) ───────────────────────────────────────────────────
rootEl: '#consenti-root', // CSS selector or HTMLElement; omit to use document.body
// ── Dark mode (optional) ─────────────────────────────────────────────────────
darkMode: false, // true = apply dark colour tokens to the widget
// ── Auto Initialize widget (optional) ────────────────────────────────────────
autoInit: true,
// ── Hide Powered By Consenti text from banner/modal (optional) ───────────────
hidePoweredBy: true,
// ── Backend API (optional) ──────────────────────────────────────────────────
api: {
enabled: true,
baseUrl: 'https://your-site.com',
authToken: '',
tenantId: 'my-site', // tenant identifier sent with API requests
complianceGroup: 'opt-in', // skip auto-resolution; always fetch this group's profile
trustDomain: false, // bypass domain allowlist check (dev/test only)
},
// ── Integrations (optional) ─────────────────────────────────────────────────
utils: {
gtm: {
containerId: 'GTM-XXXXXX',
dataLayer: 'dataLayer',
events: [], // [] = all events
urlPassthrough: true,
adsDataRedaction: false,
},
},
// ── Frontend plugins (optional) ─────────────────────────────────────────────
plugins: [],
// ── Runtime profile overrides (optional) ────────────────────────────────────
profileOverride: {
mainBanner: { position: 'top' },
},
})compliance
Selects which consent model the widget applies. When omitted, Consenti auto-detects the appropriate group from the browser's navigator.language and optional geo data.
| Key | Type | Default | Description |
|---|---|---|---|
type | string | auto-detected | Compliance group key. Either one of the 8 built-in groups (see the table below), or an arbitrary string matching a profile authored in the dashboard against a customComplianceGroup instead of a built-in group — resolved the same way as a fixed built-in group (api mode required; no pre-built fallback exists for a custom group). |
geoDataProvider | WidgetCountryResolverFn | undefined | Custom async function that returns the visitor's ISO 3166-1 alpha-2 country code. Used to improve auto-detection when the default locale-based heuristic is not accurate enough. Called once per session. |
ageGate | AgeGateWidgetConfig | undefined | Age gate configuration — { enabled, minimumAge, requireParentalConsent }. When enabled: true, the widget shows a Yes/No age-confirmation prompt before anything else (banner, GPC, CCPA opt-out). Declining denies all non-mandatory cookies immediately; with requireParentalConsent: true it also fires aconsenti:parentalConsentRequiredevent carrying a token for your own out-of-band verification flow. See the "Age Gate" section of the @consenti/ui README for the full behavior. |
tcf | TcfWidgetConfig | undefined | IAB TCF v2.2 client stub configuration — { enabled, cmpId, cmpVersion }, must match the backend's tcf config. When enabled, installswindow.__tcfapi. See the "TCF v2.2 Implementation Guide". |
Compliance groups
| type | Model | Covered regulations | Notes |
|---|---|---|---|
opt-in | Opt-in | GDPR, UK GDPR, PIPEDA, POPIA, PDPA-TH, APPI, KVKK | Banner on first visit; all non-mandatory cookies denied until granted. Default when no type given and browser locale maps to EU/EEA. |
opt-out | Opt-out | CCPA, US state laws | All cookies default to granted; consent written silently; no banner unless user visits a "Do Not Sell" page. |
opt-out-strict | Strict Opt-out | CPRA (California 2023) | Supersedes CCPA. Opt-out for sale/sharing; opt-in required for sensitive data. GPC triggers both Do Not Sell and Do Not Share. |
opt-in-dpdpa | Opt-in (DPDPA) | DPDPA (India 2023) | Fiduciary name + grievance officer rendered in modal. Age gate required for children under 18. GPC signal ignored. |
opt-in-china | Opt-in (China) | PIPL (China 2021) | Separate consent required for each processing purpose. Cross-border transfer rules enforced. |
opt-in-brazil | Opt-in (Brazil) | LGPD | 10 lawful bases; ANPD-enforced; parental consent gate for under-12. |
general-privacy-consent | General consent | TCF v2.2, COPPA, general privacy notices | Full-flexibility mode: no region-specific behaviours enforced. Configure the banner entirely via your profile. |
notice-only | Notice only | Informational / no opt-in law applies | Consent written automatically as granted for all cookies. Banner shown once as a notice, no action required. |
customComplianceGroup profiles work, see the Jurisdiction Coverage Map.import type { WidgetCountryResolverFn } from '@consenti/ui'
const geoResolver: WidgetCountryResolverFn = async () => {
const res = await fetch('/api/geo')
const { country } = await res.json()
return country // e.g. 'DE', 'US', 'IN'
}
new ConsentiSetup({
compliance: {
type: 'opt-in',
geoDataProvider: geoResolver,
},
})core
Controls widget behaviour, consent storage, and theming. All keys are optional.
Tenant & locale
| Key | Type | Default | Description |
|---|---|---|---|
tenantId | string | undefined | Logical identifier for this installation. Sent with API requests and used to namespace consent records when multiple Consenti instances share the same backend. |
locale | string | 'en' | BCP 47 locale code, e.g. 'fr', 'fr-CA'. Resolution order: exact match → language prefix → defaultLocale. |
GPC
| Key | Type | Default | Description |
|---|---|---|---|
autoHonorGPC | boolean | 'strict' | false | How to handle the browser's Global Privacy Control signal.false = ignore. true = pre-deny listenGpc cookies and show the GPC banner variant.'strict' = pre-deny and write consent silently — no banner shown. Individual profiles can also set gpcMode per-profile; this field overrides that when explicitly set. |
Profile resolution
| Key | Type | Default | Description |
|---|---|---|---|
usePrebuiltProfiles | boolean | true | When true, Consenti lazy-loads the pre-built profile chunk for the resolved compliance group. Set to false to use only profiles defined via ConsentiProfile or profileOverride. |
cacheResolvedProfiles | boolean | true | When true, the resolved profile URL from the API's /resolve-profile endpoint is cached in sessionStorage with a 1-hour TTL, avoiding redundant network requests on every page load. |
Storage
| Key | Type | Default | Description |
|---|---|---|---|
storage | 'cookie' | 'localStorage' | 'cookie' | Where consent is persisted in the browser.cookie works across subdomains when cookieDomains is set.localStorage is scoped to the exact origin and cannot be shared across subdomains. In API mode the server always issues its own cookie regardless. |
cookieDomains | string | undefined | Comma-separated domain list, e.g. '.example.com,.sub.example.com'. The first entry is used as the Domain attribute on the consent cookie, making it readable on all subdomains of that domain. |
Visitors & receipts
| Key | Type | Default | Description |
|---|---|---|---|
userId | string | undefined | Server-assigned UUID for authenticated users. When set, replaces the browser-generated visitor ID. Combined with API mode, this enables cross-device consent synchronisation — consent granted on mobile is recognised on desktop. |
allowReceipt | boolean | false | When true, a "Download consent receipt" checkbox appears in the preference modal footer. Checking it before saving triggers a JSON download containing a timestamped record of the user's choices. |
CSS & logging
| Key | Type | Default | Description |
|---|---|---|---|
disableCssTemplate | boolean | false | When true, no <style> tag is injected at all. Use this when you provide your own stylesheet and want full control over every rule. All BEM class names still apply. |
console | Array<'error' | 'warn' | 'info' | 'debug'> | ['error'] | Log levels to emit to the browser console. Pass an empty array to suppress all output. Add 'debug' during development to trace profile resolution and consent storage. |
core.theme
Inline CSS token overrides. Each key maps directly to a --consenti-* CSS custom property injected on the widget root element at runtime. You only need to set the values you want to change — unset keys keep their stylesheet default. For full CSS control, see the Themes & CSS guide.
| Key | CSS variable | Default | Description |
|---|---|---|---|
bgColor | --consenti-color-bg | #ffffff | Background colour for banners and modals. |
textColor | --consenti-color-text | #1a1a1a | Primary body text colour. |
primaryColor | --consenti-color-primary | #1565c0 | Primary button background and interactive accent colour. |
primaryTextColor | --consenti-color-primary-text | #ffffff | Text colour rendered on top of primaryColor backgrounds. |
secondaryColor | --consenti-color-secondary | #f0f4f8 | Secondary / ghost button background. |
secondaryTextColor | --consenti-color-secondary-text | #1a3460 | Text colour for secondary buttons. |
borderColor | --consenti-color-border | #e2e8f0 | Borders and dividers. |
accentColor | --consenti-color-accent | #d32f2f | Background for accent-style buttons (destructive actions). |
accentTextColor | --consenti-color-accent-text | #ffffff | Text colour on top of accentColor. |
fontFamily | --consenti-font-family | system-ui, sans-serif | Font stack applied to all widget text. |
fontSizeBase | --consenti-font-size-base | 14px | Base font size for body content. |
fontSizeHeading | --consenti-font-size-heading | inherit | Font size for banner and modal headings. |
fontSizeMultiplier | --consenti-font-size-mult | 1 | Scale multiplier applied to all font sizes. '1.1' = 10% larger throughout. |
borderRadius | --consenti-border-radius | 8px | Border-radius for banner and modal containers. |
buttonBorderRadius | --consenti-border-radius-btn | 4px | Border-radius applied to all button elements. |
toggleBgOn | --consenti-toggle-bg-on | #1565c0 | Background of toggle switches in the ON (granted) state. |
toggleBgOff | --consenti-toggle-bg-off | #cccccc | Background of toggle switches in the OFF (denied) state. |
new ConsentiSetup({
compliance: { type: 'opt-in' },
core: {
theme: {
primaryColor: '#7c3aed', // purple accent
primaryTextColor: '#ffffff',
borderRadius: '12px',
buttonBorderRadius: '999px', // pill buttons
fontFamily: 'Inter, sans-serif',
},
},
})api
Connects the widget to the Consenti backend. When enabled, consent records are posted to the API and the active profile is resolved via /resolve-profile. Disabled by default — the widget works fully offline without it.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | When true, the widget calls /resolve-profile to find the best profile for the visitor and posts consent records to the API. Falls back to a pre-built profile if the API request fails. |
baseUrl | string | window.location.origin | Root URL where @consenti/api is mounted. The widget appends /consenti/api/v1/... to this value. Set explicitly when the API is on a different domain. |
authToken | string | '' | Sent as Authorization: Bearer <token> on every API request. Leave empty for public / unauthenticated access. |
tenantId | string | undefined | Tenant identifier sent with API requests. Required when the backend serves multiple tenants. Must match the tenant configured in the dashboard. |
complianceGroup | string | undefined | When set, skips the /resolve-profile auto-resolution call and always fetches the profile for this specific compliance group. Useful for regional deployments or A/B testing. |
trustDomain | boolean | false | Bypasses the domain allowlist check on the resolved profile. Only use during local development or trusted server-side rendering. Never set to true in production. |
new ConsentiSetup({
api: {
enabled: true,
baseUrl: 'https://consent.example.com', // API is on a subdomain
},
// compliance group resolved automatically per-visitor via /resolve-profile
})new ConsentiSetup({
compliance: { type: 'opt-in' },
api: {
enabled: true,
baseUrl: 'https://consent.example.com',
complianceGroup: 'opt-in', // always fetch the GDPR-model profile
},
})api.enabled is true and the network request fails (offline, server error), the widget silently falls back to the pre-built profile for the detected compliance group, then to the built-in default. Consent submission retries are not automatic — use the events API to implement your own retry logic.utils.gtm
Google Tag Manager / Google Consent Mode v2 integration. Setting utils.gtm to any object (even {}) turns on real Consent Mode signalling: a gtag('consent', 'default', …) call as soon as the widget initializes — before any tag can fire — and gtag('consent', 'update', …) on every consent submission. This uses the standard gtag stub-queue pattern, so it works whether your own gtag.js/GTM snippet loads before or after Consenti — you do not need gtag.js already on the page.
| Key | Type | Default | Description |
|---|---|---|---|
containerId | string | undefined | GTM container ID, e.g. 'GTM-XXXXXX'. When set, Consenti injects the GTM library itself — omit if you already load GTM/gtag.js separately (Consent Mode signalling still works either way, since it only depends on utils.gtm being configured, not on this field). |
dataLayer | string | 'dataLayer' | Name of the dataLayer array on window. Override only when your site uses a custom variable name (rare). |
verbose | boolean | false | When true, additionally mirrors every consenti:* event (banner shown, modal opened, etc.) onto the dataLayer as a generic { event, content } push — for custom, non-Consent-Mode GTM triggers. Off by default, so the dataLayer only carries real consent signals. |
events | string[] | [] | Only relevant when verbose: true — narrows which event names get mirrored. An empty array (default) means all events. Has no effect on the core Consent Mode gtag('consent', …) calls, which always fire regardless. |
urlPassthrough | boolean | false | Calls gtag('set', 'url_passthrough', true)alongside every consent update. Enables Google Consent Mode v2 "cookieless pings" — Google can model conversions even when ad_storage is denied. |
adsDataRedaction | boolean | false | Calls gtag('set', 'ads_data_redaction', true) when ad_storage is denied, causing Google to redact identifying fields from ad pings. |
new ConsentiSetup({
compliance: { type: 'opt-in' },
utils: {
gtm: {
containerId: 'GTM-XXXXXX', // omit if you load GTM/gtag.js yourself
urlPassthrough: true, // cookieless conversion modelling
adsDataRedaction: true, // redact ad pings when consent denied
},
},
})plugins
An array of frontend plugin instances to initialise alongside the widget. Each plugin receives the widget's public API surface via its initialize(widget) method and can hook into consent events, inject DOM, or forward consent signals to third-party services.
import { SegmentPlugin } from '@consenti/ui-plugin-segment'
new ConsentiSetup({
compliance: { type: 'opt-in' },
plugins: [
new SegmentPlugin({ writeKey: 'YOUR_WRITE_KEY' }),
],
})See the Plugins guide for the full plugin API and available first-party plugins.
profileOverride
Accepts a Partial<ResolvedProfile> that is deep-merged on top of the resolved profile after it has been loaded (from the API, a pre-built profile, a local ConsentiProfile, or the built-in default). Only the keys you supply are applied — everything else is left unchanged.
This is a runtime override only. It does not affect the stored profile. See the Advanced Profile reference for a full reference and examples.
// Checkout page — move banner out of the way
new ConsentiSetup({
compliance: { type: 'opt-in' },
profileOverride: {
mainBanner: { position: 'right-bottom' },
},
})new ConsentiSetup({
compliance: { type: 'opt-in' },
profileOverride: {
mainBanner: {
buttons: {
'accept': { text: 'Accept', style: 'primary', action: 'custom', cookies: '*' },
'decline': { text: 'Decline', style: 'secondary', action: 'custom', cookies: '!' },
},
},
},
})Deleting a key with null
Setting a key to null removes it from the merged result instead of leaving the base value in place (JSON Merge Patch semantics). This is how you remove a single entry from a keyed map — a cookie category, a parameter — without repeating the rest of that map's contents. Omitting a key (or setting it to undefined) still means "leave the base value alone" — only an explicit null deletes.
new ConsentiSetup({
compliance: { type: 'opt-in' },
profileOverride: {
preferenceModal: {
categories: { marketing: null }, // deletes the 'marketing' category entirely
},
},
})rootEl
By default the widget appends a <div id="consenti-root"> to document.body and mounts banners and modals inside it. Use rootEl to mount into your own container instead.
| Value | Behaviour |
|---|---|
string (CSS selector) | Resolved via document.querySelector(). Throws if the element is not found. |
HTMLElement | Used directly. Throws if the element is not attached to the document. |
| omitted | Creates #consenti-root and appends it to document.body (default). |
// HTML: <div id="consent-wrapper"></div>
new ConsentiSetup({
compliance: { type: 'opt-in' },
rootEl: '#consent-wrapper', // CSS selector
// rootEl: document.getElementById('consent-wrapper')!, // or HTMLElement directly
})darkMode
Enables dark colour tokens for the widget. Setting darkMode: true adds the consenti-root--dark class to the root element, which overrides all CSS custom properties to their dark equivalents.
new ConsentiSetup({
compliance: { type: 'opt-in' },
darkMode: true,
})
// Or detect the user's OS preference:
new ConsentiSetup({
compliance: { type: 'opt-in' },
darkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
})autoInit
By default the widget begins initialising immediately when the constructor runs. Set autoInit: false to prevent this — the widget will not touch the DOM until you explicitly call widget.init().
| Key | Type | Default | Description |
|---|---|---|---|
autoInit | boolean | true | When false, the constructor returns without initialising. Call await widget.init() manually to start the widget. After destroy() you can also call init() again to re-initialise the same instance. |
const widget = new ConsentiSetup({
compliance: { type: 'opt-in' },
rootEl: '#consent-mount',
autoInit: false,
})
// Later, once the mount point exists in the DOM:
await widget.init()
widget.onReady(() => console.log('Ready:', widget.hasConsent()))Minimal configs by use case
Simplest possible — auto-detect compliance
new ConsentiSetup({ })Explicit GDPR opt-in
new ConsentiSetup({ compliance: { type: 'opt-in' } })CCPA opt-out (no banner)
new ConsentiSetup({ compliance: { type: 'opt-out' } })Cross-subdomain consent
new ConsentiSetup({
compliance: { type: 'opt-in' },
core: {
storage: 'cookie',
cookieDomains: '.example.com', // shared across app.example.com, www.example.com, etc.
},
})Authenticated user — cross-device sync
// Server renders the page with the authenticated user's UUID
new ConsentiSetup({
core: {
userId: '{{ server_user_id }}',
},
api: { enabled: true },
})GPC strict mode + GTM
new ConsentiSetup({
compliance: { type: 'opt-in' },
core: {
autoHonorGPC: 'strict', // deny silently, no banner shown
},
utils: {
gtm: { containerId: 'GTM-XXXXXX', adsDataRedaction: true },
},
})TypeScript imports
import type {
ConsentiConfig, // top-level config object
ComplianceWidgetConfig, // compliance section
AgeGateWidgetConfig, // compliance.ageGate section
TcfWidgetConfig, // compliance.tcf section
WidgetCountryResolverFn, // custom geo resolver function type
CoreConfig, // core section
ApiConfig, // api section
UtilsConfig, // utils section
GtmConfig, // utils.gtm section
ThemeConfig, // core.theme section
} from '@consenti/ui'