Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 on lines +177 to 231
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

OneTrust OnetrustActiveGroups OneTrustGroupsUpdated JavaScript API 2024

💡 Result:

OnetrustActiveGroups and OneTrustGroupsUpdated are key elements of the OneTrust Web CMP JavaScript API for managing user consent groups. OnetrustActiveGroups is a global data layer variable (window.OnetrustActiveGroups) that contains a comma-separated string of IDs for active consent groups (those with user consent or always active). It is populated after script initialization and updated on consent changes. Example usage for Google Consent Mode: window.OnetrustActiveGroups.includes(',C0002,') ? 'granted' : 'denied' OneTrustGroupsUpdated is a window event triggered on script load/refresh and when user consent preferences update. It provides an array of active group IDs via event.detail. window.addEventListener("OneTrustGroupsUpdated", event => { console.log(Active groups: ${event.detail.join(", ")}); }); These are used together for real-time consent-aware logic, such as enabling/disabling trackers. Official docs confirm this as of 2024 references in related pages. No specific "OneTrust OnetrustActiveGroups OneTrustGroupsUpdated JavaScript API 2024" endpoint; these are client-side CMP features.

Citations:


🌐 Web query:

Cookiebot consent object JavaScript API 2024 CookiebotOnAccept CookiebotOnDecline

💡 Result:

The Cookiebot CMP (Consent Management Platform) provides a JavaScript API via the global 'Cookiebot' object, loaded by including the script https://consent.cookiebot.com/uc.js?cbid=YOUR_CBID (replace YOUR_CBID with your domain group ID from your Cookiebot account). Key elements: Consent Object (Cookiebot.consent): Read-only properties indicating user consent for cookie categories: - consent.necessary (bool, default true): Necessary cookies accepted. - consent.preferences (bool, default false): Preferences cookies accepted. - consent.statistics (bool, default false): Statistics cookies accepted. - consent.marketing (bool, default false): Marketing cookies accepted. - consented (bool, default false): Any cookies accepted. - declined (bool, default false): Cookies declined. - hasResponse (bool): User has responded. - method (string): 'implied' or 'explicit' consent method. Methods: - Cookiebot.show: Show consent dialog. - Cookiebot.renew: Renew/change consent (resets and shows dialog). - Cookiebot.runScripts: Execute blocked scripts based on consent (for dynamic content). - Cookiebot.getScript(url, [async], [callback]): Load external script respecting consent. Events (use window.addEventListener): - CookiebotOnAccept: Fires when user accepts cookies (also on page load if previously accepted). - CookiebotOnDecline: Fires when user declines cookies (also on page load if previously declined). - CookiebotOnLoad: When consent loaded (not script load). - CookiebotOnConsentReady: Consent state ready. - CookiebotOnDialogInit: Dialog initialized. Callbacks (define global functions before script loads): - CookiebotCallback_OnAccept: Called on accept. - CookiebotCallback_OnDecline: Called on decline. - CookiebotCallback_OnLoad: Called when consent loaded. Example usage for OnAccept (load GA if statistics consented): window.addEventListener('CookiebotOnAccept', function { if (Cookiebot.consent.statistics) { // Load Google Analytics } }); function CookiebotCallback_OnDecline { // Handle decline } This API remains consistent as of 2025-2026 sources from official Cookiebot developer pages and support docs. Always check consent before loading trackers: if (Cookiebot.consent.statistics) { ... }. For server-side, parse CookieConsent cookie.

Citations:


Fix OneTrust group ID matching pattern.

The Cookiebot implementation correctly uses the Cookiebot.consent object structure (statistics, marketing properties) and listens to the proper events (CookiebotOnAccept/CookiebotOnDecline).

However, the OneTrust code uses an unsafe string matching pattern. Since OnetrustActiveGroups is a comma-separated string of group IDs, groups.includes('C0002') can produce false positives if another group ID contains 'C0002' as a substring (e.g., 'C00020'). Use comma delimiters instead:

Recommended OneTrust pattern
groups.includes(',C0002,') ? 'granted' : 'denied'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/docs/1.guides/3.consent.md` around lines 163 - 221, The OneTrust
handler uses a substring match on OnetrustActiveGroups which can false-positive
(e.g., 'C00020' matching 'C0002'); in the apply function that reads (window as
any).OnetrustActiveGroups and calls consent.update, change the check from
groups.includes('C0002') / 'C0004' to a delimiter-aware match — e.g., normalize
the groups string by adding surrounding commas or split into an array and then
check for exact IDs (use ',C0002,' / ',C0004,' includes or
array.includes('C0002')) before setting analytics_storage / ad_storage /
ad_user_data / ad_personalization in consent.update.

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