Consenti

UI Widget — Plugin System

Plugins let you extend the Consenti widget without modifying its source. Each plugin receives the full typed ConsentiWidgetAPI in initialize() — call any public method, read the resolved profile, access DOM elements, and listen to events.

ℹ️A broken plugin never blocks the consent flow. Errors in initialize(), destroy(), and all lifecycle hooks are caught and logged as console.warn — the widget continues normally.

Minimal plugin

ts
import { ConsentiPlugin, ConsentiSetup } from '@consenti/ui'
import type { ConsentiWidgetAPI } from '@consenti/ui'

class MyPlugin extends ConsentiPlugin {
  initialize(widget: ConsentiWidgetAPI): void {
    // widget is the live ConsentiSetup instance — call anything on it
    console.log('Widget ready. Has consent:', widget.hasConsent())
  }

  destroy(): void {
    // clean up here — runs when widget.destroy() is called
  }
}

const widget = new ConsentiSetup({
  core: { profileId: 0 },
  plugins: [new MyPlugin()],
})

ConsentiWidgetAPI reference

The ConsentiWidgetAPI interface is what your plugin receives.ConsentiSetup implements it, so every method is always available.

State inspection

MethodReturnsDescription
hasConsent()booleanTrue if a valid consent record exists
getConsent()ConsentValue | nullThe stored consent map keyed by cookie ID
getConsentDate()Date | falseDate of the most recent consent submission
getGTMConsent()Record<string,string> | nullConsent in Google Consent Mode v2 format
bannerVisibility()'main' | 'gpc' | falseWhich banner variant is currently visible
modalVisibility()'preference' | falseWhether the preference modal is open
getProfile()ResolvedProfile | nullThe resolved profile: cookies, banners, modal config, version

UI control

MethodDescription
showBanner(gpc?)Show the main or GPC banner
hideBanner()Hide the banner
showModal(triggerEl?)Open the preference modal
hideModal()Close the preference modal
submitConsent(consent)Submit consent programmatically
deleteConsent()Delete the consent record
reConsent()Delete consent and re-open the banner

DOM access

MethodReturnsDescription
getRootElement()HTMLElement | null#consenti-root — the container for all widget DOM
getBannerElement()HTMLElement | null#consenti-banner — the banner element (may be hidden)
getModalElement()HTMLElement | null#consenti-modal — the preference modal element

Lifecycle hooks (optional)

Override these methods to react to consent events. All hooks are optional — only define the ones you need.

HookWhen it fires
initialize(widget)Required. After ConsentiSetup.init() completes. May be async.
destroy()Required. When widget.destroy() is called.
onConsentSubmit(consent)After consent is saved to storage and POSTed to the API
onBannerShow()After the banner becomes visible
onBannerHide()After the banner is hidden
onModalShow()After the preference modal opens
onModalHide()After the preference modal closes

Example: Analytics plugin

Track consent events with your analytics provider:

ts
import { ConsentiPlugin } from '@consenti/ui'
import type { ConsentiWidgetAPI, ConsentValue } from '@consenti/ui'

class AnalyticsPlugin extends ConsentiPlugin {
  private widget!: ConsentiWidgetAPI

  initialize(widget: ConsentiWidgetAPI): void {
    this.widget = widget
  }

  destroy(): void {}

  onConsentSubmit(consent: ConsentValue): void {
    const profile = this.widget.getProfile()
    myAnalytics.track('consent_saved', {
      profileId: profile?.id,
      profileVersion: profile?.version,
      ...consent,
    })
  }

  onBannerShow(): void {
    myAnalytics.track('consent_banner_shown')
  }
}

Example: DOM extension plugin

Add custom content or styles to the banner without touching Consenti source:

ts
import { ConsentiPlugin } from '@consenti/ui'
import type { ConsentiWidgetAPI } from '@consenti/ui'

class BrandingPlugin extends ConsentiPlugin {
  private logoEl: HTMLElement | null = null

  initialize(widget: ConsentiWidgetAPI): void {
    // Inject custom CSS variable on the root container
    const root = widget.getRootElement()
    if (root) {
      root.style.setProperty('--consenti-color-primary', '#0057b8')
    }
  }

  onBannerShow(): void {
    const banner = document.getElementById('consenti-banner')
    if (!banner || banner.querySelector('.my-logo')) return

    this.logoEl = document.createElement('img')
    this.logoEl.src = '/logo.png'
    this.logoEl.className = 'my-logo'
    this.logoEl.alt = 'My Brand'
    this.logoEl.style.cssText = 'height:24px;margin-bottom:8px;display:block;'
    banner.prepend(this.logoEl)
  }

  onBannerHide(): void {
    this.logoEl?.remove()
    this.logoEl = null
  }

  destroy(): void {
    this.logoEl?.remove()
    this.logoEl = null
  }
}

Example: Profile inspection plugin

Use getProfile() to read cookie definitions and make runtime decisions:

ts
import { ConsentiPlugin } from '@consenti/ui'
import type { ConsentiWidgetAPI } from '@consenti/ui'

class ConditionalLoadPlugin extends ConsentiPlugin {
  initialize(widget: ConsentiWidgetAPI): void {
    const profile = widget.getProfile()
    if (!profile) return

    const cookieIds = profile.cookies.map((c) => c.id)
    console.log('Profile v' + profile.version + ' manages:', cookieIds)

    // Load a script only when analytics_storage is granted
    if (widget.getConsent()?.analytics_storage === 'granted') {
      this.loadAnalyticsScript()
    }

    window.addEventListener('consenti:consentSubmitted', () => {
      if (widget.getConsent()?.analytics_storage === 'granted') {
        this.loadAnalyticsScript()
      }
    })
  }

  private loadAnalyticsScript(): void {
    if (document.getElementById('analytics-script')) return
    const script = document.createElement('script')
    script.id = 'analytics-script'
    script.src = 'https://analytics.example.com/tracker.js'
    document.head.appendChild(script)
  }

  destroy(): void {}
}

Example: Async plugin

initialize() can be async — the widget awaits it before resolving widget.ready:

ts
import { ConsentiPlugin } from '@consenti/ui'
import type { ConsentiWidgetAPI } from '@consenti/ui'

class RemoteConfigPlugin extends ConsentiPlugin {
  async initialize(widget: ConsentiWidgetAPI): Promise<void> {
    const res = await fetch('/api/consent-config')
    const config = await res.json() as { bannerDelay?: number }

    if (config.bannerDelay && !widget.hasConsent()) {
      await new Promise<void>((resolve) => setTimeout(resolve, config.bannerDelay))
      widget.showBanner()
    }
  }

  destroy(): void {}
}

Registering plugins

ts
import { ConsentiSetup } from '@consenti/ui'
import { AnalyticsPlugin } from './plugins/analytics'
import { BrandingPlugin } from './plugins/branding'

const widget = new ConsentiSetup({
  core: { profileId: 1, regulation: 'gdpr' },
  plugins: [
    new AnalyticsPlugin(),
    new BrandingPlugin(),
  ],
})
⚠️Plugins run in the order they are listed. If an async initialize() is slow, it delays the rest of widget startup. Keep remote calls in plugins fast, or fire them without await if they are non-blocking.

TypeScript tip

Import ConsentiWidgetAPI as a type-only import to keep your plugin decoupled from the ConsentiSetup class:

ts
import { ConsentiPlugin } from '@consenti/ui'
import type { ConsentiWidgetAPI } from '@consenti/ui'
//           ^^^^ type-only import — no runtime dependency on ConsentiSetup

class MyPlugin extends ConsentiPlugin {
  initialize(widget: ConsentiWidgetAPI) { /* ... */ }
  destroy() {}
}