UI Widget — Advanced Profile
A profile defines everything the Consenti widget renders: which cookies / purposes it manages, the banner and modal layout, all button labels and actions, and the text copy for every supported locale. It is the single source of truth for the UI — not the widget config itself. New here? Start with the Profile quick start instead — this page is the complete reference: resolution order, every ConsentiProfile field, multi-locale, GPC banner, and more.
How profiles are resolved
Every time new ConsentiSetup(config) initialises, it resolves exactly one profile through one of six scenarios. The compliance group (from compliance.type or auto-detected from the browser) determines which pre-built profile is loaded.
| Scenario | API | compliance.type | Resolution |
|---|---|---|---|
| 1A | off | auto-detect | Browser locale + optional geo resolver → compliance group → pre-built profile chunk (dynamic import) |
| 1B | off | fixed (e.g. 'opt-in') | Pre-built profile chunk for that group, loaded via dynamic import |
| 2A | on | auto-detect | GET /resolve-profile returns the best profile URL for the visitor; result cached in sessionStorage (1 h TTL) |
| 2B | on | fixed — a built-in group (e.g. 'opt-in') or a custom string | Fetches the profile hot-served for that group name — skips auto-resolution. A custom string targets a dashboard profile authored with a customComplianceGroup instead of one of the 8 built-in groups; falls back to scenario 1B (which has no pre-built profile for a custom name, so it throws) if nothing is active there |
| Local | any | any | ConsentiProfile registered in code; takes precedence over pre-built profiles when found for the matching compliance type |
| Fallback | any | any | Built-in default GDPR profile (Google Consent Mode v2, four purposes) — always available |
new ConsentiSetup({ api: { enabled: true } })
│
├─ 2A. GET /resolve-profile ──▶ success → fetch returned profile URL (cached 1h)
│ fail ↓
├─ Local: ConsentiProfile in registry ──▶ found → use local profile
│ missing ↓
├─ 1A. Pre-built profile (auto-detect) ──▶ loaded → use pre-built chunk
│ fail ↓
└─ Fallback: built-in default ──▶ always availableprofileOverride are merged on top. See profileOverride below.Pre-built profiles
Consenti ships pre-built profiles for each compliance group. They are loaded as dynamic import chunks — only the chunk for the resolved compliance group is downloaded. Each pre-built profile covers all four Google Consent Mode v2 purposes plus a mandatory functional bucket.
The fallback built-in profile (used when all resolution steps fail) is an English opt-in GDPR profile — a realistic starting point for any GTM-backed site.
// Built-in fallback — used when all resolution steps fail. expiryDays is set once,
// profile-wide (see ProfileConfig.expiryDays below) — not per cookie.
const defaultCookies = {
functionality_storage: {}, // legal basis comes from its category ('mandatory')
analytics_storage: { listenGpc: true },
ad_storage: { listenGpc: true },
ad_user_data: { listenGpc: true },
ad_personalization: { listenGpc: true },
}To customise the pre-built profile banner without defining a full profile, use profileOverride.
ConsentiProfile — custom profiles in code
Use ConsentiProfile to define profiles entirely in JavaScript. Each instance is registered in a module-level registry under a unique Symbol, returned by profile.getType().
new ConsentiSetup() — the registry lookup happens during resolution. Pass compliance: { type: profile.getType() } to use it directly, or set complianceGroup: 'opt-in' (etc.) on the profile config with compliance: { type: 'opt-in' } (or 'auto') to have it automatically preferred over the built-in profile whenever that group is resolved — see Overriding a built-in group below.Minimal example
import { ConsentiProfile, ConsentiSetup } from '@consenti/ui'
const profile = new ConsentiProfile({
defaultLocale: 'en',
expiryDays: 365, // profile-wide consent expiry — replaces the old per-cookie `expiry`
cookies: {
necessary: {},
analytics: { listenGpc: true },
marketing: { listenGpc: true },
},
translations: {
en: {
mainBanner: {
position: 'bottom',
heading: 'We value your privacy',
htmlText: 'We use cookies to improve your experience.',
buttons: {
'accept-all': { text: 'Accept All', style: 'primary', action: 'custom', cookies: '*' },
'reject-optional': { text: 'Reject Optional', style: 'secondary', action: 'custom', cookies: '!' },
'customize': { text: 'Customize', style: 'secondary', action: 'manage' },
},
},
preferenceModal: {
heading: 'Cookie Preferences',
subheading: 'Choose which cookies you allow.',
position: 'center',
showClose: true,
overlayOpacity: 50,
buttons: {
'accept-all': { text: 'Accept All', style: 'primary', action: 'custom', cookies: '*' },
'save-preferences': { text: 'Save Preferences', style: 'primary', action: 'submit' },
'reject-optional': { text: 'Reject Optional', style: 'text', action: 'custom', cookies: '!' },
},
categories: {
necessary: {
heading: 'Strictly Necessary',
htmlText: 'Required for the site to function.',
legalBasis: 'mandatory',
cookies: ['necessary'],
},
analytics: {
heading: 'Analytics',
htmlText: 'Helps us understand how visitors use the site.',
legalBasis: 'consent',
cookies: ['analytics'],
},
marketing: {
heading: 'Marketing',
htmlText: 'Used to personalise ads and measure campaign performance.',
legalBasis: 'consent',
cookies: ['marketing'],
},
},
},
},
},
})
new ConsentiSetup({
compliance: { type: profile.getType() },
// ConsentiProfile auto-registered above takes precedence over the pre-built profile
})Multi-locale profile
Add as many locale keys as needed. The widget resolves locale by trying the exact BCP 47 code, then the language prefix, then defaultLocale.
const profile = new ConsentiProfile({
defaultLocale: 'en',
cookies: {
necessary: {},
analytics: {},
},
translations: {
en: {
mainBanner: {
position: 'bottom',
htmlText: 'We use cookies.',
buttons: {
'accept-all': { text: 'Accept All', style: 'primary', action: 'custom', cookies: '*' },
'reject-optional': { text: 'Reject Optional', style: 'secondary', action: 'custom', cookies: '!' },
},
},
preferenceModal: {
heading: 'Cookie Preferences',
buttons: { 'save-preferences': { text: 'Save Preferences', style: 'primary', action: 'submit' } },
categories: {
necessary: { heading: 'Necessary', htmlText: 'Required.', legalBasis: 'mandatory', cookies: ['necessary'] },
analytics: { heading: 'Analytics', htmlText: 'Usage stats.', legalBasis: 'consent', cookies: ['analytics'] },
},
},
},
fr: {
mainBanner: {
position: 'bottom',
htmlText: 'Nous utilisons des cookies.',
buttons: {
'tout-accepter': { text: 'Tout accepter', style: 'primary', action: 'custom', cookies: '*' },
'tout-refuser': { text: 'Tout refuser', style: 'secondary', action: 'custom', cookies: '!' },
},
},
preferenceModal: {
heading: 'Préférences cookies',
buttons: { enregistrer: { text: 'Enregistrer', style: 'primary', action: 'submit' } },
categories: {
necessary: { heading: 'Nécessaire', htmlText: 'Requis.', legalBasis: 'mandatory', cookies: ['necessary'] },
analytics: { heading: 'Analytiques', htmlText: 'Statistiques.', legalBasis: 'consent', cookies: ['analytics'] },
},
},
},
},
})
new ConsentiSetup({
compliance: { type: profile.getType() },
core: { locale: 'fr' },
})GPC banner variant
Add a gpcBanner key alongside mainBanner in a locale to show a dedicated banner when the Global Privacy Control signal is detected. Falls back to mainBanner if not provided.
translations: {
en: {
mainBanner: { /* ... */ },
gpcBanner: {
position: 'bottom',
heading: 'Privacy signal detected',
htmlText: "Your browser's GPC signal was detected. Ad cookies have been pre-denied.",
showClose: false,
buttons: {
'understood': { text: 'Understood', style: 'primary', action: 'custom', cookies: '!' },
'customize': { text: 'Customize', style: 'secondary', action: 'manage' },
},
},
preferenceModal: { /* ... */ },
},
}Overriding a built-in compliance group
Instead of switching compliance.type to your profile's own Symbol, you can set complianceGroup on a registered profile and Consenti will prefer it automatically over the built-in embedded profile whenever geo-detection ('auto') or an explicit compliance.type resolves to that group — no change needed at the ConsentiSetup call site.
import { ConsentiProfile, ConsentiSetup } from '@consenti/ui'
// Full replace (deepMerge: false, the default) — this config is used as-is
// whenever the resolved group is 'opt-in'; the built-in profile is never even fetched.
new ConsentiProfile({
complianceGroup: 'opt-in',
defaultLocale: 'en',
cookies: { necessary: {}, analytics: { listenGpc: true } },
translations: { /* ... full mainBanner / preferenceModal ... */ },
})
// Patch just one field (deepMerge: true) — only the fields you supply are merged
// onto the built-in 'opt-in' profile; everything else (cookies, categories, other
// locales) comes from the built-in as normal.
new ConsentiProfile({
complianceGroup: 'opt-in',
deepMerge: true,
translations: {
en: { mainBanner: { position: 'right-bottom' } },
},
})
new ConsentiSetup({ compliance: { type: 'auto' } }) // or { type: 'opt-in' } — either resolves the override aboveProfileConfig — all options
| Field | Type | Required | Description |
|---|---|---|---|
defaultLocale | string | Yes | BCP 47 locale used when the requested locale has no translation. E.g. 'en'. |
cookies | CookieMap | No | Consent parameters, keyed by ID (the map key is the ID — no separate id field). See Cookie type below. |
expiryDays | number | No | Days until consent expires and the visitor is asked again — profile-wide, replacing the old per-cookie expiry. Default: 365. |
translations | Record<string, LocaleTranslations> | No | Banner and modal config keyed by locale. If omitted, the widget falls back to profileOverride or defaults. |
mainBanner | MainBanner | Yes | Default banner content, used when a locale-specific override isn't provided in translations. |
gpcBanner | GpcBanner | No | Default GPC banner content (same shape as MainBanner), used when a locale-specific override isn't provided. Falls back to mainBanner if neither is set. |
preferenceModal | PreferenceModal | Yes | Default modal content, including the categories map. See Category type below. |
allowedOrigins | string[] | No | Hostnames permitted to submit consent for this profile. Only enforced server-side in API mode. Supports *.example.com wildcards. Empty = allow all. |
dpdpa | DpdpaConfig | No | Required when using the opt-in-dpdpa compliance group. See the DPDPA guide. |
complianceGroup | ComplianceGroupId | No | Marks this registered profile as an override for a built-in compliance group — see Overriding a built-in compliance group above. |
deepMerge | boolean | No | Only meaningful together with complianceGroup. false (default) fully replaces the built-in profile; true deep-merges this config onto it instead. |
gpcMode | 'ignore' | 'honor' | 'strict' | No | Per-profile GPC handling. Overridden at runtime by core.autoHonorGPCwhen that's explicitly set. |
allowReceipt | boolean | No | Shows the "Download consent receipt" checkbox in the modal footer. See preferenceModal.receiptLabel/receiptDescription to customise its text. |
hidePoweredBy | boolean | No | Suppresses the "Powered by Consenti" footer link in the banner and modal. Defaults to true (hidden) when unset. |
darkMode | boolean | No | Default dark-mode state for this profile. Overridden at runtime by ConsentiConfig.darkMode or setDarkMode(). |
complianceConfig | Record<string, string> | No | Per-compliance extra config, e.g. the DPDPA data-fiduciary name. |
showFooterMetadata | boolean | No | Shows a metadata footer strip (Consent ID, Date, Version, Privacy Settings link) in the banner/modal. |
enhanceAccessibility | boolean | No | Applies WCAG 2.1 AA button sizing (44px min-height), visible focus rings, and screen-reader labels. |
Cookie type
cookies is a Record<string, Cookie> — the map key is the parameter's ID (used as the key in the stored consent map, e.g. cookies.analytics_storage). There is no idfield on the value itself. A parameter's legal basis (mandatory / consent / legitimate interest) is not set here — it's derived from whichever Category lists this ID in its cookies array. Every parameter must belong to exactly one category.
| Field | Type | Required | Description |
|---|---|---|---|
purpose | CookiePurpose | No | Purpose classification (e.g. 'analytics', 'marketing') used for informational display in the modal. |
listenGpc | boolean | No | When true and GPC handling is active, this parameter is auto-denied/objected when the browser GPC signal is present. |
preGrant | boolean | No | Pre-grants this parameter's default consent to 'granted' (instead of the compliance-group default) when no stored decision exists yet. Only meaningful when the owning category has legalBasis: 'consent' — mandatory/legitimate-interest categories are already effectively pre-granted. Never overrides an active GPC signal. Default: false. |
tcfVendorId | number | No | IAB TCF vendor ID, shown in the modal when TCF display is relevant. |
tcfPurposes | number[] | No | IAB TCF purpose IDs this parameter serves. |
tcfSpecialFeatures | number[] | No | IAB TCF special feature IDs this parameter uses. |
cpraCategory | 'sale' | 'sharing' | 'sensitive' | No | CPRA classification. Only relevant for the opt-out-strict compliance group. |
preGrant: true on a strict opt-in compliance group (opt-in, opt-in-dpdpa, opt-in-china, opt-in-brazil) triggers a server-side compliance warning — those regulations require consent to be denied by default. Only use it for custom profiles targeting jurisdictions where pre-granted consent is valid.LocaleTranslations type
| Field | Type | Required | Description |
|---|---|---|---|
mainBanner | MainBanner | Yes | Banner shown on first visit or when no consent exists. |
gpcBanner | GpcBanner | No | Banner shown instead of mainBanner when GPC is detected. Same shape as MainBanner. Falls back to mainBanner if omitted. |
preferenceModal | PreferenceModal | Yes | Slide-in panel where the user manages individual cookie categories. |
MainBanner / GpcBanner type
| Field | Type | Required | Description |
|---|---|---|---|
position | 'bottom' | 'top' | 'middle' | 'left-bottom' | 'right-bottom' | Yes | Where the banner is anchored on screen. |
htmlText | string | Yes | Body content. HTML is allowed. Sanitise any user-supplied values externally. |
heading | string | No | Heading text. Omit to hide the heading element entirely. |
headingTag | string | No | HTML tag for the heading, e.g. 'div' (default) or 'p'. |
overlayOpacity | number | No | Full-page overlay opacity, 0–100. 0 disables the overlay. Default: 0. |
showClose | boolean | No | Render a ✕ close button that dismisses the banner without saving consent. |
showLocaleSwitcher | boolean | No | Show a locale switcher in the banner. Only renders when the profile has more than one locale defined. |
buttons | Button[] | Yes | Ordered list of action buttons rendered in the banner footer. |
PreferenceModal type
| Field | Type | Required | Description |
|---|---|---|---|
buttons | Button[] | Yes | Footer action buttons. |
categories | CategoryMap | Yes | Consent categories keyed by ID, each shown with its own tri-state toggle plus one toggle per member parameter. See Category type below. |
position | 'center' | 'left' | 'right' | No | Modal panel anchor. Default: 'center'. |
heading | string | No | Modal heading text. |
subheading | string | No | Optional subheading rendered below the heading. |
htmlText | string | No | Optional intro text rendered above the category list. HTML is allowed. |
overlayOpacity | number | No | Modal backdrop opacity, 0–100. Default: 50. |
showClose | boolean | No | Show a ✕ close button in the modal header. |
showLocaleSwitcher | boolean | No | Show a locale switcher in the modal header. Only renders when the profile has more than one locale defined. |
persistent | boolean | No | When true, clicking the overlay does not close the modal. The user must click a footer button. Default: false. |
mobileFullScreenBreakpoint | number | No | Screen width in px at or below which the modal expands to fill the entire screen. Default: 576. Set to 0 to disable full-screen on mobile entirely. |
headingTag | string | No | HTML tag for the modal heading. |
receiptLabel | string | No | Label for the consent-receipt download checkbox (shown when allowReceipt is true). Falls back to a default when omitted. |
receiptDescription | string | No | Description shown beneath the consent-receipt checkbox. Falls back to a default when omitted. |
Button type
| Field | Type | Required | Description |
|---|---|---|---|
id | string | No | Machine id, rendered as the button's DOM id (consenti-btn-{id}) so integrators can target a specific button. Set automatically when authored via a UI template; omitted buttons get no DOM id. |
text | string | Yes | Label rendered on the button. |
style | 'primary' | 'secondary' | 'text' | 'accent' | Yes | Visual appearance only — never affects click behaviour.primary = filled accent; secondary = outlined/ghost; text = no border, link-like; accent = destructive red (uses --consenti-accent-color). |
action | 'custom' | 'manage' | 'submit' | 'close' | 'link' | Yes | What happens on click. custom = grant/deny the IDs in cookies; manage = open the preference modal; submit = save the current modal toggle state; close = dismiss without saving; link = navigate to url (opens in a new tab). |
cookies | '*' | '!' | string[] | When action: 'custom' | Which cookie IDs to affect. '*' = grant all; '!' = deny all; string[] = specific IDs only. |
url | string | When action: 'link' | Target URL for the link button. Opens in a new tab. Typically used for privacy policy or terms links rendered below the banner body. |
Category type
categories is a Record<string, Category> — the map key is the category's ID. A Category is the single source of legal basis for every parameter it lists — Cookie itself has no legal-basis field.
| Field | Type | Required | Description |
|---|---|---|---|
heading | string | Yes | Category name shown as the master toggle label. |
htmlText | string | Yes | Description text beneath the heading. HTML is allowed. |
cookies | string[] | Yes | IDs of the parameters (from the profile's cookies map) that belong to this category. Each renders its own toggle inside the category; a parameter must belong to exactly one category. |
legalBasis | 'mandatory' | 'consent' | 'legitimate_interest' | Yes | Replaces the old separate mandatory/type fields — single source of truth. 'mandatory' = every member parameter is always 'granted', toggle disabled. 'consent' = standard opt-in/opt-out. 'legitimate_interest' = on by default until objected; refusal is stored as 'objected' instead of 'denied'. |
legitimateInterestDescription | string | No | Only meaningful when legalBasis === 'legitimate_interest'. Shown in the modal to explain the legitimate interest claimed — the GDPR balancing-test justification, authored per-locale alongside the category's other content. |
headingTag | string | No | HTML tag for the category heading, e.g. 'div' (default). |
The category's own master toggle is 3-state — denied, allowed, or partial(mixed) — computed live from its member parameters' individual toggle states, not stored separately. Clicking a partial toggle resolves to fully allowed first (standard tri-state checkbox convention), then denied on a second click.
profileOverride — runtime overrides
profileOverride lets you change any field of the resolved profile at runtime, without touching the base profile definition. It is applied as a deep merge on top of whatever profile was resolved (default, local, or API).
Common uses: adjusting banner position per-page, swapping button copy for A/B tests, or running the widget with no backend at all by providing the entire profile inline.
profileOverride accepts a Partial<ResolvedProfile>, deep-merged key by key. Since cookies and preferenceModal.categories are keyed maps, you can add or override a single parameter/category ID without needing to repeat every other one.Override banner copy and buttons only
new ConsentiSetup({
compliance: { type: 'opt-in' },
profileOverride: {
mainBanner: {
heading: 'Cookie notice',
htmlText: 'We use analytics cookies to improve the site.',
buttons: {
'accept': { text: 'Accept', style: 'primary', action: 'custom', cookies: '*' },
'decline': { text: 'Decline', style: 'secondary', action: 'custom', cookies: '!' },
},
},
},
})Change banner position per page
// On the checkout page, keep the banner out of the way
new ConsentiSetup({
compliance: { type: 'opt-in' },
profileOverride: {
mainBanner: { position: 'right-bottom' },
},
})Full inline profile — no backend, no ConsentiProfile
You can provide an entire profile through profileOverride alone. This is the most concise approach for simple frontend-only installations that do not need multi-locale support.
new ConsentiSetup({
compliance: { type: 'opt-in' },
profileOverride: {
cookies: {
necessary: {},
analytics: { listenGpc: true },
marketing: { listenGpc: true },
},
expiryDays: 365,
mainBanner: {
position: 'bottom',
heading: 'We value your privacy',
htmlText: 'We use cookies to improve your experience and personalise ads.',
buttons: {
'accept-all': { text: 'Accept All', style: 'primary', action: 'custom', cookies: '*' },
'reject-optional': { text: 'Reject Optional', style: 'secondary', action: 'custom', cookies: '!' },
'customize': { text: 'Customize', style: 'secondary', action: 'manage' },
},
},
gpcBanner: {
position: 'bottom',
heading: 'Privacy signal detected',
htmlText: "Your browser's GPC signal has been honoured. Ad cookies are pre-denied.",
buttons: {
'understood': { text: 'Understood', style: 'primary', action: 'custom', cookies: '!' },
'customize': { text: 'Customize', style: 'secondary', action: 'manage' },
},
},
preferenceModal: {
position: 'center',
heading: 'Cookie Preferences',
subheading: 'Choose which cookies you allow us to use.',
showClose: true,
overlayOpacity: 50,
buttons: {
'accept-all': { text: 'Accept All', style: 'primary', action: 'custom', cookies: '*' },
'save-preferences': { text: 'Save Preferences', style: 'primary', action: 'submit' },
'reject-optional': { text: 'Reject Optional', style: 'text', action: 'custom', cookies: '!' },
},
categories: {
necessary: {
heading: 'Strictly Necessary',
htmlText: 'Required for the site to function. Cannot be disabled.',
legalBasis: 'mandatory',
cookies: ['necessary'],
},
analytics: {
heading: 'Analytics',
htmlText: 'Helps us understand how visitors use the site (e.g. Google Analytics).',
legalBasis: 'consent',
cookies: ['analytics'],
},
marketing: {
heading: 'Marketing',
htmlText: 'Used to deliver relevant ads and measure campaign effectiveness.',
legalBasis: 'consent',
cookies: ['marketing'],
},
},
},
},
})Override modal categories only
new ConsentiSetup({
compliance: { type: 'opt-in' },
profileOverride: {
preferenceModal: {
categories: {
necessary: {
heading: 'Strictly Necessary',
htmlText: 'Core functionality. Always enabled.',
legalBasis: 'mandatory',
cookies: ['necessary'],
},
analytics: {
heading: 'Analytics',
htmlText: 'Page view and interaction tracking.',
legalBasis: 'consent',
cookies: ['analytics'],
},
},
},
},
})Using the backend API
When the Consenti API is deployed, profiles are managed in the Admin Dashboard and grouped by compliance type. The widget calls GET /resolve-profile to find the best profile for each visitor automatically.
// Auto-resolve: server picks the right profile for the visitor's locale / country
new ConsentiSetup({
api: {
enabled: true,
baseUrl: 'https://your-site.com', // where @consenti/api is mounted
},
core: { locale: 'fr' },
})
// Fixed group: always fetch the GDPR-model profile
new ConsentiSetup({
api: {
enabled: true,
baseUrl: 'https://your-site.com',
complianceGroup: 'opt-in',
},
})profileOverride on top of an API profile — for example to change the banner position on a specific page without creating a new profile variant in the dashboard.TypeScript imports
import type {
ProfileConfig, // passed to new ConsentiProfile(config)
RegisterableProfileConfig, // ProfileConfig, or a { complianceGroup, deepMerge: true, ... } overlay
LocaleTranslations, // translations[locale] shape
MainBanner, // mainBanner / gpcBanner shape
PreferenceModal, // preferenceModal shape
Button, // button definition
ButtonStyle, // 'primary' | 'secondary' | 'text' | 'accent'
ButtonAction, // 'custom' | 'manage' | 'submit' | 'close' | 'link'
Category, // single category definition
CategoryMap, // Record<string, Category> — preferenceModal.categories shape
Cookie, // single parameter definition
CookieMap, // Record<string, Cookie> — cookies shape
ResolvedProfile, // what profileOverride accepts (Partial<ResolvedProfile>)
ComplianceWidgetConfig, // compliance section
AgeGateWidgetConfig, // compliance.ageGate section
TcfWidgetConfig, // compliance.tcf section
WidgetCountryResolverFn, // custom geo resolver function type
NonEmptyArray, // [T, ...T[]] — at least one element
} from '@consenti/ui'