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
| Event | Fired when |
|---|---|
consenti:bannerInitialized | Widget initialises and determines whether to show the banner |
consenti:bannerVisibility | Banner shows or hides |
consenti:modalVisibility | Preference modal shows or hides |
consenti:consentBeingSubmitted | User clicked a consent button (before API call) |
consenti:consentSubmitted | Consent saved (cookie written + API call if configured) |
consenti:parentalConsentRequired | Age gate declined with requireParentalConsent: true — carries a parentalConsentToken; see the COPPA guide. |
consenti:forgetMeRequested | widget.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:forgotten | Consent 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.
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 referenceRaw DOM listeners
Raw window.addEventListener calls work too and can coexist with on():
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
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
interface BannerVisibilityDetail {
visible: boolean // true = banner appeared; false = banner hidden
variant: 'main' | 'gpc' // which banner
action: boolean // true = triggered by user button click
}ModalVisibilityDetail
interface ModalVisibilityDetail {
visible: boolean // true = modal opened; false = modal closed
action: boolean // true = triggered by user button click
}ConsentSubmittedDetail
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
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:
// 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 deniedutils.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.
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.
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.
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.
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).
<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:
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:
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())