---
title: "Cookie Consent"
description: "Gate scripts behind user consent and drive vendor-native consent APIs through a typed per-script `consent` object."
canonical_url: "https://scripts.nuxt.com/docs/guides/consent"
last_updated: "2026-10-11T11:20:08.992Z"
---

::callout{icon="i-heroicons-play" to="https://stackblitz.com/github/nuxt/scripts/tree/main/examples/cookie-consent" target="_blank"}
Try the live [Cookie Consent Example](https://stackblitz.com/github/nuxt/scripts/tree/main/examples/cookie-consent) or [Granular Consent Example](https://stackblitz.com/github/nuxt/scripts/tree/main/examples/granular-consent) on [StackBlitz](https://stackblitz.com).
::

## 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:

1. **[`useScriptTriggerConsent()`{lang="ts"}](/docs/api/use-script-trigger-consent)**: 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()`{lang="ts"}: 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](https://developers.google.com/tag-platform/security/guides/consent), while Matomo uses [`setConsentGiven`/`forgetConsentGiven`](https://developer.matomo.org/guides/tracking-consent). 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.

::code-group
```ts [utils/cookie.ts]
export const scriptsConsent = useScriptTriggerConsent()
```

```vue [app.vue]
<script setup lang="ts">
import { scriptsConsent } from '#imports'

useScript('https://example.com/analytics.js', {
  trigger: scriptsConsent,
})
</script>
```

```vue [components/cookie-banner.vue]
<script setup lang="ts">
import { scriptsConsent } from '#imports'
</script>

<template>
  <button @click="scriptsConsent.accept()">
    Accept Cookies
  </button>
</template>
```
::

### Reactive source

Pass a `Ref<boolean>`{lang="html"} if an external store owns the state.

```ts
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.

```vue
<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

```ts
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()`{lang="ts"} 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)`{lang="ts"}. Use the `defaultConsent` option for the initial state: `consent.default()`{lang="ts"} 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`.

```ts
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[]`{lang="html"} (GCMv2)                     | `consent.default(state)`{lang="ts"} / `consent.update(state)`{lang="ts"}                                           |
| Google Tag Manager | `ConsentState \| ConsentState[]`{lang="html"} (GCMv2)                     | `consent.default(state)`{lang="ts"} / `consent.update(state)`{lang="ts"}                                           |
| Bing UET           | `{ ad_storage }`                                                          | `consent.update({ ad_storage })`{lang="ts"}                                                                        |
| Meta Pixel         | `'granted' \| 'denied'`                                                   | `consent.grant()`{lang="ts"} / `consent.revoke()`{lang="ts"}                                                       |
| TikTok Pixel       | `'granted' \| 'denied' \| 'hold'`                                         | `consent.grant()`{lang="ts"} / `consent.revoke()`{lang="ts"} / `consent.hold()`{lang="ts"}                         |
| Matomo             | `'required' \| 'given' \| 'not-required'`                                 | `consent.give()`{lang="ts"} / `consent.forget()`{lang="ts"} *(requires `defaultConsent: 'required'` or `'given'`)* |
| Mixpanel           | `'opt-in' \| 'opt-out'`                                                   | `consent.optIn()`{lang="ts"} / `consent.optOut()`{lang="ts"}                                                       |
| PostHog            | `'opt-in' \| 'opt-out'`                                                   | `consent.optIn()`{lang="ts"} / `consent.optOut()`{lang="ts"}                                                       |
| Clarity            | `boolean` *(the current schema also admits an undocumented record value)* | `consent.set(value)`{lang="ts"}                                                                                    |

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:

```ts
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:

```ts
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](https://developer.onetrust.com/onetrust/docs/single-page-applications)

```ts
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](https://www.cookiebot.com/en/developer/)

```ts
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)
})
```

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
