Consenti

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.

ScenarioAPIcompliance.typeResolution
1Aoffauto-detectBrowser locale + optional geo resolver → compliance group → pre-built profile chunk (dynamic import)
1Bofffixed (e.g. 'opt-in')Pre-built profile chunk for that group, loaded via dynamic import
2Aonauto-detectGET /resolve-profile returns the best profile URL for the visitor; result cached in sessionStorage (1 h TTL)
2Bonfixed — a built-in group (e.g. 'opt-in') or a custom stringFetches 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
LocalanyanyConsentiProfile registered in code; takes precedence over pre-built profiles when found for the matching compliance type
FallbackanyanyBuilt-in default GDPR profile (Google Consent Mode v2, four purposes) — always available
Resolution flow (API enabled)
text
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 available
ℹ️After resolution, any properties set in profileOverride 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.

Default profile cookies (fallback)
ts
// 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().

💡Define the profile before calling 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

ts
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.

ts
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.

ts
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.

ts
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 above

ProfileConfig — all options

FieldTypeRequiredDescription
defaultLocalestringYesBCP 47 locale used when the requested locale has no translation. E.g. 'en'.
cookiesCookieMapNoConsent parameters, keyed by ID (the map key is the ID — no separate id field). See Cookie type below.
expiryDaysnumberNoDays until consent expires and the visitor is asked again — profile-wide, replacing the old per-cookie expiry. Default: 365.
translationsRecord<string, LocaleTranslations>NoBanner and modal config keyed by locale. If omitted, the widget falls back to profileOverride or defaults.
mainBannerMainBannerYesDefault banner content, used when a locale-specific override isn't provided in translations.
gpcBannerGpcBannerNoDefault GPC banner content (same shape as MainBanner), used when a locale-specific override isn't provided. Falls back to mainBanner if neither is set.
preferenceModalPreferenceModalYesDefault modal content, including the categories map. See Category type below.
allowedOriginsstring[]NoHostnames permitted to submit consent for this profile. Only enforced server-side in API mode. Supports *.example.com wildcards. Empty = allow all.
dpdpaDpdpaConfigNoRequired when using the opt-in-dpdpa compliance group. See the DPDPA guide.
complianceGroupComplianceGroupIdNoMarks this registered profile as an override for a built-in compliance group — see Overriding a built-in compliance group above.
deepMergebooleanNoOnly meaningful together with complianceGroup. false (default) fully replaces the built-in profile; true deep-merges this config onto it instead.
gpcMode'ignore' | 'honor' | 'strict'NoPer-profile GPC handling. Overridden at runtime by core.autoHonorGPCwhen that's explicitly set.
allowReceiptbooleanNoShows the "Download consent receipt" checkbox in the modal footer. See preferenceModal.receiptLabel/receiptDescription to customise its text.
hidePoweredBybooleanNoSuppresses the "Powered by Consenti" footer link in the banner and modal. Defaults to true (hidden) when unset.
darkModebooleanNoDefault dark-mode state for this profile. Overridden at runtime by ConsentiConfig.darkMode or setDarkMode().
complianceConfigRecord<string, string>NoPer-compliance extra config, e.g. the DPDPA data-fiduciary name.
showFooterMetadatabooleanNoShows a metadata footer strip (Consent ID, Date, Version, Privacy Settings link) in the banner/modal.
enhanceAccessibilitybooleanNoApplies WCAG 2.1 AA button sizing (44px min-height), visible focus rings, and screen-reader labels.

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.

FieldTypeRequiredDescription
purposeCookiePurposeNoPurpose classification (e.g. 'analytics', 'marketing') used for informational display in the modal.
listenGpcbooleanNoWhen true and GPC handling is active, this parameter is auto-denied/objected when the browser GPC signal is present.
preGrantbooleanNoPre-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.
tcfVendorIdnumberNoIAB TCF vendor ID, shown in the modal when TCF display is relevant.
tcfPurposesnumber[]NoIAB TCF purpose IDs this parameter serves.
tcfSpecialFeaturesnumber[]NoIAB TCF special feature IDs this parameter uses.
cpraCategory'sale' | 'sharing' | 'sensitive'NoCPRA classification. Only relevant for the opt-out-strict compliance group.
⚠️Setting 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

FieldTypeRequiredDescription
mainBannerMainBannerYesBanner shown on first visit or when no consent exists.
gpcBannerGpcBannerNoBanner shown instead of mainBanner when GPC is detected. Same shape as MainBanner. Falls back to mainBanner if omitted.
preferenceModalPreferenceModalYesSlide-in panel where the user manages individual cookie categories.

MainBanner / GpcBanner type

FieldTypeRequiredDescription
position'bottom' | 'top' | 'middle' | 'left-bottom' | 'right-bottom'YesWhere the banner is anchored on screen.
htmlTextstringYesBody content. HTML is allowed. Sanitise any user-supplied values externally.
headingstringNoHeading text. Omit to hide the heading element entirely.
headingTagstringNoHTML tag for the heading, e.g. 'div' (default) or 'p'.
overlayOpacitynumberNoFull-page overlay opacity, 0–100. 0 disables the overlay. Default: 0.
showClosebooleanNoRender a ✕ close button that dismisses the banner without saving consent.
showLocaleSwitcherbooleanNoShow a locale switcher in the banner. Only renders when the profile has more than one locale defined.
buttonsButton[]YesOrdered list of action buttons rendered in the banner footer.

PreferenceModal type

FieldTypeRequiredDescription
buttonsButton[]YesFooter action buttons.
categoriesCategoryMapYesConsent 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'NoModal panel anchor. Default: 'center'.
headingstringNoModal heading text.
subheadingstringNoOptional subheading rendered below the heading.
htmlTextstringNoOptional intro text rendered above the category list. HTML is allowed.
overlayOpacitynumberNoModal backdrop opacity, 0–100. Default: 50.
showClosebooleanNoShow a ✕ close button in the modal header.
showLocaleSwitcherbooleanNoShow a locale switcher in the modal header. Only renders when the profile has more than one locale defined.
persistentbooleanNoWhen true, clicking the overlay does not close the modal. The user must click a footer button. Default: false.
mobileFullScreenBreakpointnumberNoScreen 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.
headingTagstringNoHTML tag for the modal heading.
receiptLabelstringNoLabel for the consent-receipt download checkbox (shown when allowReceipt is true). Falls back to a default when omitted.
receiptDescriptionstringNoDescription shown beneath the consent-receipt checkbox. Falls back to a default when omitted.

Button type

FieldTypeRequiredDescription
idstringNoMachine 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.
textstringYesLabel rendered on the button.
style'primary' | 'secondary' | 'text' | 'accent'YesVisual 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'YesWhat 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.
urlstringWhen 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.

FieldTypeRequiredDescription
headingstringYesCategory name shown as the master toggle label.
htmlTextstringYesDescription text beneath the heading. HTML is allowed.
cookiesstring[]YesIDs 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'YesReplaces 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'.
legitimateInterestDescriptionstringNoOnly 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.
headingTagstringNoHTML 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

ts
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

ts
// 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.

ts
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

ts
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.

ts
// 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',
  },
})
💡You can still use 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

ts
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'