Skip to main content
Guides

Cookie Consent

Try the live Cookie Consent Example or Granular Consent Example on StackBlitz.

Consent affects two separate moments: whether the browser loads a script, and which consent state an already-loaded vendor receives. Nuxt Scripts exposes one API for each:

  1. useScriptTriggerConsent(): a binary load gate for the normal main-thread path. The script only starts loading after consent is granted. The experimental Partytown path is an exception because it writes its tag during SSR and ignores trigger timing.
  2. Per-script consent object returned from every consent-aware useScriptX(): the vendor's typed API for granting, revoking, or updating consent after load. Its defaultConsent option sets the policy before the vendor's init call.

Nuxt Scripts maps each integration to the vendor's vocabulary where practical. For example, Google Analytics and Google Tag Manager use Consent Mode v2, while Matomo uses setConsentGiven/forgetConsentGiven. The table below lists the Nuxt Scripts surface; it is not a shared cross-vendor standard.

Binary load gate

For a binary cookie banner, pass one consent trigger to every main-thread script that must wait for acceptance. Do not use the current Partytown path for a script whose tag must be held until opt-in.

export const scriptsConsent = useScriptTriggerConsent()

Reactive source

Pass a Ref<boolean> if an external store owns the state.

const agreedToCookies = ref(false)
const consent = useScriptTriggerConsent({ consent: agreedToCookies })

Revoking

Consent revocation flips the reactive consented ref. Once the load-gate promise resolves, the script has loaded; watch consented if you need to tear it down after revocation.

<template>
  <div v-if="scriptsConsent.consented.value">
    <button @click="scriptsConsent.revoke()">
      Revoke Consent
    </button>
  </div>
  <button v-else @click="scriptsConsent.accept()">
    Accept Cookies
  </button>
</template>
const consent = useScriptTriggerConsent({
  consent: agreedToCookies,
  postConsentTrigger: () => new Promise<void>(resolve =>
    setTimeout(resolve, 3000),
  ),
})

Revoking during that three-second delay changes consented to false, but it does not cancel the pending timer; the trigger still resolves when the timer finishes.

Every consent-aware useScriptX() returns a consent object typed to the vendor's API. defaultConsent sets the initial policy in clientInit, before the vendor's first call. Your cookie banner can then call consent.* to update it.

Google Analytics and Google Tag Manager also expose the raw runtime command consent.default(state). Use the defaultConsent option for the initial state: consent.default() runs after the composable's clientInit. At that point, Analytics has queued its js and config commands, while Tag Manager has queued its gtm.start / gtm.js event. Both Google consent methods validate against the Consent Mode v2 schema and warn through consola about unknown keys or values other than granted and denied.

const { consent } = useScriptGoogleAnalytics({
  id: 'G-XXXXXXXX',
  defaultConsent: { ad_storage: 'denied', analytics_storage: 'denied' },
})

function onAcceptAll() {
  consent.update({
    ad_storage: 'granted',
    ad_user_data: 'granted',
    ad_personalization: 'granted',
    analytics_storage: 'granted',
  })
}

Per-vendor surface

ScriptdefaultConsentRuntime consent.*
Google AnalyticsConsentState | ConsentState[] (GCMv2)consent.default(state) / consent.update(state)
Google Tag ManagerConsentState | ConsentState[] (GCMv2)consent.default(state) / consent.update(state)
Bing UET{ ad_storage }consent.update({ ad_storage })
Meta Pixel'granted' | 'denied'consent.grant() / consent.revoke()
TikTok Pixel'granted' | 'denied' | 'hold'consent.grant() / consent.revoke() / consent.hold()
Matomo'required' | 'given' | 'not-required'consent.give() / consent.forget() (requires defaultConsent: 'required' or 'given')
Mixpanel'opt-in' | 'opt-out'consent.optIn() / consent.optOut()
PostHog'opt-in' | 'opt-out'consent.optIn() / consent.optOut()
Clarityboolean (the current schema also admits an undocumented record value)consent.set(value)

See each script's registry page for vendor-specific caveats.

Fanning out to multiple scripts

Vendors do not share a normalized consent model. When one banner controls several scripts, call each consent API explicitly:

const ga = useScriptGoogleAnalytics({ id: 'G-XXX', defaultConsent: { ad_storage: 'denied', analytics_storage: 'denied' } })
const meta = useScriptMetaPixel({ id: '123', defaultConsent: 'denied' })
const matomo = useScriptMatomoAnalytics({ cloudId: 'foo.matomo.cloud', defaultConsent: 'required' })

function onAcceptAll() {
  ga.consent.update({
    ad_storage: 'granted',
    ad_user_data: 'granted',
    ad_personalization: 'granted',
    analytics_storage: 'granted',
  })
  meta.consent.grant()
  matomo.consent.give()
}

function onDeclineAll() {
  ga.consent.update({
    ad_storage: 'denied',
    ad_user_data: 'denied',
    ad_personalization: 'denied',
    analytics_storage: 'denied',
  })
  meta.consent.revoke()
  matomo.consent.forget()
}

Granular categories

For separate analytics and marketing choices, map each choice only to the categories that vendor understands:

function savePreferences(choices: { analytics: boolean, marketing: boolean }) {
  ga.consent.update({
    analytics_storage: choices.analytics ? 'granted' : 'denied',
    ad_storage: choices.marketing ? 'granted' : 'denied',
    ad_user_data: choices.marketing ? 'granted' : 'denied',
    ad_personalization: choices.marketing ? 'granted' : 'denied',
  })
  if (choices.marketing)
    meta.consent.grant()
  else meta.consent.revoke()
  if (choices.analytics)
    matomo.consent.give()
  else matomo.consent.forget()
}

Third-party CMP recipes

If a Consent Management Platform owns the UI, bridge its events into each script's consent API.

OneTrust

const ga = useScriptGoogleAnalytics({ id: 'G-XXX', defaultConsent: { ad_storage: 'denied', analytics_storage: 'denied' } })
const meta = useScriptMetaPixel({ id: '123', defaultConsent: 'denied' })

onNuxtReady(() => {
  function apply() {
    const groups = (window as any).OnetrustActiveGroups as string | undefined
    if (!groups)
      return
    const activeGroups = new Set(groups.split(',').filter(Boolean))
    const analytics = activeGroups.has('C0002')
    const marketing = activeGroups.has('C0004')
    ga.consent.update({
      analytics_storage: analytics ? 'granted' : 'denied',
      ad_storage: marketing ? 'granted' : 'denied',
      ad_user_data: marketing ? 'granted' : 'denied',
      ad_personalization: marketing ? 'granted' : 'denied',
    })
    if (marketing)
      meta.consent.grant()
    else meta.consent.revoke()
  }

  apply()
  window.addEventListener('OneTrustGroupsUpdated', apply)
})

Cookiebot

const ga = useScriptGoogleAnalytics({ id: 'G-XXX', defaultConsent: { ad_storage: 'denied', analytics_storage: 'denied' } })
const meta = useScriptMetaPixel({ id: '123', defaultConsent: 'denied' })

onNuxtReady(() => {
  function apply() {
    const cb = (window as any).Cookiebot
    if (!cb?.consent)
      return
    ga.consent.update({
      analytics_storage: cb.consent.statistics ? 'granted' : 'denied',
      ad_storage: cb.consent.marketing ? 'granted' : 'denied',
      ad_user_data: cb.consent.marketing ? 'granted' : 'denied',
      ad_personalization: cb.consent.marketing ? 'granted' : 'denied',
    })
    if (cb.consent.marketing)
      meta.consent.grant()
    else meta.consent.revoke()
  }

  apply()
  window.addEventListener('CookiebotOnConsentReady', apply)
  window.addEventListener('CookiebotOnAccept', apply)
  window.addEventListener('CookiebotOnDecline', apply)
})