Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 169 additions & 45 deletions docs/content/docs/1.guides/3.consent.md
Original file line number Diff line number Diff line change
@@ -1,108 +1,232 @@
---
title: Consent Management
description: Learn how to get user consent before loading scripts.
description: Gate scripts behind user consent and drive vendor-native consent APIs through a typed per-script `consent` object.
---

::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).
::

## Background
## Two complementary primitives

Many third-party scripts include tracking cookies that require user consent under privacy laws. Nuxt Scripts simplifies this process with the [`useScriptTriggerConsent()`{lang="ts"}](/docs/api/use-script-trigger-consent){lang="ts"} composable, allowing scripts to load only after you receive user consent.
Nuxt Scripts ships two consent primitives that work together:

## Usage
1. **[`useScriptTriggerConsent()`{lang="ts"}](/docs/api/use-script-trigger-consent)**: a binary load gate. The script only starts loading after consent is granted.
2. **Per-script `consent` object** returned from every consent-aware `useScriptX()`{lang="ts"}. A vendor-native, typed API for granting, revoking, or updating consent categories at runtime. Paired with each script's `defaultConsent` option for the initial policy applied *before* the vendor's init call.

The [`useScriptTriggerConsent()`{lang="ts"}](/docs/api/use-script-trigger-consent){lang="ts"} composable offers flexible interaction options suitable for various scenarios.
Each vendor exposes its own consent dialect (Google Consent Mode v2 for GA/GTM/Bing, binary grant/revoke for Meta, three-state for TikTok, `setConsentGiven`/`forgetConsentGiven` for Matomo, `opt_in`/`opt_out` for Mixpanel/PostHog, cookie toggle for Clarity). You wire each explicitly.

See the [API](/docs/api/use-script-trigger-consent) docs for full details on the available options.
## Binary load gate

### Accepting as a Function

The easiest way to make use of [`useScriptTriggerConsent()`{lang="ts"}](/docs/api/use-script-trigger-consent){lang="ts"} is by invoking the `accept` method when user consent is granted.

For an example of how you might lay out your code to handle this, see the following:
The simplest usage matches the classic cookie-banner flow: load the script only after the user clicks accept.

::code-group

```ts [utils/cookie.ts]
export const agreedToCookiesScriptConsent = useScriptTriggerConsent()
export const scriptsConsent = useScriptTriggerConsent()
```

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

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

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

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

::

### Accepting as a resolvable boolean
### Reactive source

Alternatively, you can pass a reactive reference to the consent state to the [`useScriptTriggerConsent()`{lang="ts"}](/docs/api/use-script-trigger-consent){lang="ts"} composable. This will automatically load the script when the consent state is `true`.
Pass a `Ref<boolean>`{lang="html"} if an external store owns the state.

```ts
const agreedToCookies = ref(false)
useScript('https://www.google-analytics.com/analytics.js', {
trigger: useScriptTriggerConsent({
consent: agreedToCookies
})
})
const consent = useScriptTriggerConsent({ consent: agreedToCookies })
```

### Revoking Consent
### Revoking

You can revoke consent after it has been granted using the `revoke` method. Use the reactive `consented` ref to track the current consent state.

```vue [components/cookie-banner.vue]
<script setup lang="ts">
import { agreedToCookiesScriptConsent } from '#imports'
</script>
Consent revocation flips the reactive `consented` ref. Once the load-gate promise has resolved the script has loaded; watch `consented` if you need to tear down on revoke.

```vue
<template>
<div v-if="agreedToCookiesScriptConsent.consented.value">
<p>Cookies accepted</p>
<button @click="agreedToCookiesScriptConsent.revoke()">
<div v-if="scriptsConsent.consented.value">
<button @click="scriptsConsent.revoke()">
Revoke Consent
</button>
</div>
<button v-else @click="agreedToCookiesScriptConsent.accept()">
<button v-else @click="scriptsConsent.accept()">
Accept Cookies
</button>
</template>
```

### Delaying script load after consent
### Delaying the load after consent

```ts
const consent = useScriptTriggerConsent({
consent: agreedToCookies,
postConsentTrigger: () => new Promise<void>(resolve =>
setTimeout(resolve, 3000),
),
})
```

There may be instances where you want to trigger the script load after a certain event, only if the user has consented.
## Per-script consent API

For this you can use the `postConsentTrigger`, which shares the same API as `trigger` from the [`useScript()`{lang="ts"}](/docs/api/use-script){lang="ts"} composable.
Every consent-aware `useScriptX()`{lang="ts"} returns a `consent` object typed to the vendor's native API. Combine it with `defaultConsent` for the initial policy (applied in `clientInit` before the vendor fires its first call) and call `consent.*` from your cookie banner to update.

```ts
const agreedToCookies = ref(false)
useScript('https://www.google-analytics.com/analytics.js', {
trigger: useScriptTriggerConsent({
consent: agreedToCookies,
// load 3 seconds after consent is granted
postConsentTrigger: () => new Promise<void>(resolve =>
setTimeout(resolve, 3000),
),
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 | `Partial<ConsentState>`{lang="html"} (GCMv2) | `consent.update(state)`{lang="ts"} |
| Google Tag Manager | `Partial<ConsentState>`{lang="html"} (GCMv2) | `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 \| Record<string, string>`{lang="html"} | `consent.set(value)`{lang="ts"} |

See each script's registry page for notes on lossy projections and vendor caveats.

### Fanning out to multiple scripts

When one cookie banner drives several vendors, wire them explicitly in your accept handler. No magic, fully typed, no lossy remapping:

```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() {
meta.consent.revoke()
matomo.consent.forget()
}
```

### Granular categories

If users can toggle categories individually (analytics, marketing, functional), the same pattern applies; each script gets only the categories it 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

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

### OneTrust

```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 analytics = groups.includes('C0002')
const marketing = groups.includes('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

```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('CookiebotOnAccept', apply)
window.addEventListener('CookiebotOnDecline', apply)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```
16 changes: 6 additions & 10 deletions docs/content/scripts/bing-uet.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,18 @@ function trackSignup() {

### Consent Mode

Bing UET supports [advanced consent mode](https://help.ads.microsoft.com/#apex/ads/en/60119/1-500). Use `onBeforeUetStart` to set the default consent state before the script loads. If consent is denied, UET only sends anonymous data.
Bing UET supports [advanced consent mode](https://help.ads.microsoft.com/#apex/ads/en/60119/1-500). Only `ad_storage` is honoured; set the initial state with `defaultConsent` and update at runtime via `consent.update()`{lang="ts"}:

```vue
<script setup lang="ts">
const { proxy } = useScriptBingUet({
onBeforeUetStart(uetq) {
uetq.push('consent', 'default', {
ad_storage: 'denied',
})
},
const { consent } = useScriptBingUet({
defaultConsent: { ad_storage: 'denied' },
})

function grantConsent() {
proxy.uetq.push('consent', 'update', {
ad_storage: 'granted',
})
consent.update({ ad_storage: 'granted' })
}
</script>
```

`onBeforeUetStart` remains available for any other pre-load setup.
36 changes: 36 additions & 0 deletions docs/content/scripts/clarity.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,39 @@ links:

::script-types
::

## Consent Mode

Clarity supports a cookie consent toggle (boolean) or an advanced consent vector (record). Set the initial value with `defaultConsent` and call `consent.set()`{lang="ts"} at runtime:

```vue
<script setup lang="ts">
const { consent } = useScriptClarity({
id: 'YOUR_PROJECT_ID',
defaultConsent: false, // disable cookies until user opts in
})

function acceptAnalytics() {
consent.set(true)
}
</script>
```

`consent.set()`{lang="ts"} also accepts Clarity's advanced consent vector for fine-grained cookie categories:

```ts
const { consent } = useScriptClarity({
id: 'YOUR_PROJECT_ID',
defaultConsent: {
ad_storage: 'denied',
analytics_storage: 'granted',
},
})

consent.set({
ad_storage: 'granted',
analytics_storage: 'granted',
})
```

See [Clarity cookie consent](https://learn.microsoft.com/en-us/clarity/setup-and-installation/cookie-consent) for details.
38 changes: 38 additions & 0 deletions docs/content/scripts/google-analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,44 @@ proxy.gtag('event', 'page_view')

The proxy exposes the `gtag` and `dataLayer` properties, and you should use them following Google Analytics best practices.

### Consent Mode

Google Analytics natively consumes [GCMv2 consent state](https://developers.google.com/tag-platform/security/guides/consent). Set the default with `defaultConsent` (fires `gtag('consent', 'default', ...)`{lang="ts"} before `gtag('js', ...)`{lang="ts"}) and call `consent.update()`{lang="ts"} at runtime:

```vue
<script setup lang="ts">
const { consent } = useScriptGoogleAnalytics({
id: 'G-XXXXXXXX',
defaultConsent: {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
},
})

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

function savePreferences(choices: { analytics: boolean, marketing: boolean }) {
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',
})
}
</script>
```

`consent.update()`{lang="ts"} accepts any `Partial<ConsentState>`{lang="ts"}; missing categories stay at their current value. For pre-`gtag('js')`{lang="ts"} setup beyond consent defaults, `onBeforeGtagStart` remains available as a general escape hatch.

### Customer/Consumer ID Tracking

For e-commerce or multi-tenant applications where you need to track customer-specific analytics alongside your main tracking:
Expand Down
Loading
Loading