Consenti

UI Widget — Events

Consenti fires custom DOM events on window at every consent lifecycle step. All events are prefixed consenti: and carry a typed detail payload.

Event reference

EventFired when
consenti:bannerInitializedWidget initialises and determines whether to show the banner
consenti:bannerVisibilityBanner shows or hides
consenti:modalVisibilityPreference modal shows or hides
consenti:consentBeingSubmittedUser clicked a consent button (before API call)
consenti:consentSubmittedConsent saved (cookie written + API call if configured)
consenti:parentalConsentRequiredAge gate declined with requireParentalConsent: true — carries a parentalConsentToken; see the COPPA guide.
consenti:forgetMeRequestedwidget.forgetMe()called (e.g. the preference modal's "Forget me" button), right before the erasure call goes out — the hook for a host app to kick off its own identity-verified erasure workflow across other systems. See the Right to Erasure guide.
consenti:forgottenConsent record erased and the banner/age-gate re-prompted, as the last step of widget.forgetMe().

Typed API — on() / off()

The recommended way to subscribe to events is widget.on() / widget.off(). This API is typed, handles the CustomEvent unwrapping for you, and cleans up automatically when widget.destroy() is called. The consenti: prefix on the event name is optional.

ts
import type { ConsentEvent } from '@consenti/ui'

const handler = (data: ConsentEvent) => {
  console.log('Consent saved:', data.consentJson)
  console.log('Action:', data.consentAction)
  console.log('GPC detected:', data.gpcDetected)
}

widget.on('consentSubmitted', handler)      // 'consenti:' prefix is optional
widget.off('consentSubmitted', handler)     // must pass the same function reference

Raw DOM listeners

Raw window.addEventListener calls work too and can coexist with on():

ts
import type { ConsentEvent } from '@consenti/ui'

window.addEventListener('consenti:consentSubmitted', (e: Event) => {
  const detail = (e as CustomEvent<ConsentEvent>).detail
  console.log('Consent action:', detail.consentAction) // 'accept_all' | 'reject_all' | 'custom' | 'update'
  console.log('Consent values:', detail.consentJson)   // { analytics: 'granted', marketing: 'denied', ... }
  console.log('Page URL:', detail.pageUrl)
  console.log('GPC detected:', detail.gpcDetected)
})

Detail types

BannerInitializedDetail

ts
interface BannerInitializedDetail {
  profileId: string
  complianceGroup?: ComplianceType
  hasExistingConsent: boolean  // true if valid consent cookie exists
  gpcDetected: boolean
  willShow: boolean            // true if banner will be rendered
}

BannerVisibilityDetail

ts
interface BannerVisibilityDetail {
  visible: boolean               // true = banner appeared; false = banner hidden
  variant: 'main' | 'gpc' // which banner
  action: boolean             // true = triggered by user button click
}

ModalVisibilityDetail

ts
interface ModalVisibilityDetail {
  visible: boolean               // true = modal opened; false = modal closed
  action: boolean             // true = triggered by user button click
}

ConsentSubmittedDetail

ts
interface ConsentSubmittedDetail {
  consentId: string                        // UUID per submission
  visitorId: string                        // visitor UUID (stable across sessions)
  profileId: string
  consentJson: Record<string, ConsentStatus>  // { analytics: 'granted', ... }
  consentAction: 'accept_all' | 'reject_all' | 'custom' | 'update'
  gpcDetected: boolean
  pageUrl: string                          // window.location.href at submission time
  timestamp: number                        // Unix timestamp, trimmed to seconds
  fromBroadcast?: boolean                  // true if this instance learned of the change via a cross-tab broadcast
  apiResponse: ConsentDbRecord             // backend response if api.enabled: true
}

ForgetMeRequestedDetail / ForgottenDetail

ts
interface ForgetMeRequestedDetail {
  visitorId: string
  profileId: string
  timestamp: number  // Unix timestamp, trimmed to seconds
}

// ForgottenDetail has the exact same shape — fired after erasure completes.
interface ForgottenDetail {
  visitorId: string
  profileId: string
  timestamp: number
}

GTM / Google Consent Mode v2

When utils.gtm is configured (see the Advanced Configuration page for every utils.gtm option), Consenti calls the real gtag() consent API — via the standard stub-queue pattern, so it works whether your own gtag.js/GTM snippet loads before or after Consenti:

js
// On initialization — before any tag can fire (default denied state)
gtag('consent', 'default', {
  analytics_storage: 'denied',
  ad_storage: 'denied',
  ad_user_data: 'denied',
  ad_personalization: 'denied',
  functionality_storage: 'granted',
  personalization_storage: 'denied',
  security_storage: 'granted',
})

// On submission
gtag('consent', 'update', {
  analytics_storage: 'granted',
  ad_storage: 'denied',
  ad_user_data: 'denied',
  ad_personalization: 'denied',
  functionality_storage: 'granted',
  personalization_storage: 'denied',
  security_storage: 'granted',
})

// Plus, when configured:
gtag('set', 'url_passthrough', true)
gtag('set', 'ads_data_redaction', true) // true when ad_storage is denied
ℹ️Set utils.gtm.verbose: true to additionally mirror every consenti:* event onto the dataLayer as a generic { event, content } push — useful for custom, non-Consent-Mode GTM triggers. Off by default.

ConsentScript — auto-load scripts on consent

ConsentScript watches consenti:consentSubmitted and injects or removes a <script> tag based on whether a specific cookie ID is granted. Consent is also evaluated immediately at construction time so existing consent is honoured without waiting for the next submission event.

ts
import { ConsentScript } from '@consenti/ui'

// bind: true (default) — auto-removes script on consent revoke, re-injects on re-grant
new ConsentScript({
  cookieId: 'analytics_storage',
  widget,
  src: 'https://cdn.example.com/analytics.js',
  onLoad: () => console.log('Analytics loaded'),
  onRevoke: () => console.log('Analytics removed'),
})

// bind: false — check consent once at construction; never attach a change listener
new ConsentScript({
  cookieId: 'analytics_storage',
  widget,
  src: 'https://cdn.example.com/analytics.js',
  bind: false,
})

ConsentAction — run a callback based on one parameter's consent

Same lifecycle as ConsentScript (evaluates immediately at construction, re-evaluates on consenti:consentSubmitted, destroy() to unbind) but fires onGrant/onDeny callbacks instead of injecting a <script>. Use this for SDKs that expose their own opt-in/opt-out method (Segment, Mixpanel, Amplitude, Sentry, …) rather than a script tag to toggle.

ts
import { ConsentAction } from '@consenti/ui'

new ConsentAction({
  id: 'analytics_storage',
  widget,
  onGrant: (params) => analyticsSdk.optIn(),
  onDeny: (params) => analyticsSdk.optOut(),
})

CategoryAction — run a callback based on a category's rollup state

Same shape as ConsentAction, bound to a whole category instead of one parameter. A category counts as granted only when every parameter it contains is 'granted'onDeny fires for both a fully-denied and a partially-granted (mixed) category.

ts
import { CategoryAction } from '@consenti/ui'

new CategoryAction({
  id: 'marketing',
  widget,
  onGrant: (params) => adSdk.enableAll(),
  onDeny: (params) => adSdk.disableAll(),
})

CategoryScript — load a script based on a category's rollup state

Like ConsentScript, but gated on a whole category being fully granted instead of one parameter.

ts
import { CategoryScript } from '@consenti/ui'

new CategoryScript({
  categoryId: 'marketing',
  widget,
  src: 'https://example.com/ad-pixel.js',
})

scanConsentScripts — declarative, zero-JS gating via data attributes

Mark an inert <script type="text/plain"> tag with data-consenti-consent-script or data-consenti-category-script and Consenti gates it automatically — no hand-written ConsentScript/CategoryScript call needed. Runs automatically once per ConsentiSetup.init() cycle (respects autoInit: false — only runs once the widget is actually initialized).

html
<script type="text/plain" data-consenti-category-script="marketing" src="https://example.com/pixel.js"></script>

<script type="text/plain" data-consenti-consent-script="analytics_storage">
  /* inline snippet, injected verbatim when granted */
</script>

<!-- data-consenti-bind="false" — evaluate once, never auto-remove/re-inject -->
<script type="text/plain" data-consenti-consent-script="ad_storage" data-consenti-bind="false" src="..."></script>

Call it again manually after dynamically adding more tags (e.g. from a late-loading CMS widget) — each tag is only ever scanned once, so re-calling is safe:

ts
import { scanConsentScripts } from '@consenti/ui'

scanConsentScripts(widget)

BannerTrigger — open banner/modal from any element

Attach a trigger to any existing element or let Consenti auto-create a button:

ts
import { BannerTrigger } from '@consenti/ui'

// Attach to existing element
new BannerTrigger({ widget, el: '#footer-cookie-settings', action: 'modal' })

// Or auto-create a button and place it yourself
const trigger = new BannerTrigger({ widget, action: 'banner', label: 'Cookie Settings' })
document.querySelector('#footer')?.appendChild(trigger.getElement())