Cookie Consent
Try the live Cookie Consent Example or Granular Consent Example on StackBlitz.
Choose the consent layer
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:
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.- Per-script
consentobject returned from every consent-awareuseScriptX(): the vendor's typed API for granting, revoking, or updating consent after load. ItsdefaultConsentoption 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>
Delaying the load after consent
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.
Per-script consent API
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
| Script | defaultConsent | Runtime consent.* |
|---|---|---|
| Google Analytics | ConsentState | ConsentState[] (GCMv2) | consent.default(state) / consent.update(state) |
| Google Tag Manager | ConsentState | 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() |
| Clarity | boolean (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)
})