Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
231 changes: 187 additions & 44 deletions docs/content/docs/1.guides/3.consent.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Consent Management
description: Learn how to get user consent before loading scripts.
description: Gate scripts behind user consent, drive Google Consent Mode v2 granular state, and fan it out to registry scripts.
---

::callout{icon="i-heroicons-play" to="https://stackblitz.com/github/nuxt/scripts/tree/main/examples/cookie-consent" target="_blank"}
Expand All @@ -9,100 +9,243 @@

## Background

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.
Many third-party scripts set tracking cookies that require user consent. Nuxt Scripts ships a single composable, [`useScriptConsent()`{lang="ts"}](/docs/api/use-script-consent){lang="ts"}, that handles both:

## Usage
1. The binary load gate: the script only starts loading after consent is granted.
2. Granular Google Consent Mode v2 state: categories like `ad_storage`, `analytics_storage`, etc., are tracked reactively and fanned out to subscribed registry scripts via their `consentAdapter`.

Check warning on line 15 in docs/content/docs/1.guides/3.consent.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "are tracked". Consider rewriting in active voice

Check warning on line 15 in docs/content/docs/1.guides/3.consent.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "are tracked". Consider rewriting in active voice

The [`useScriptTriggerConsent()`{lang="ts"}](/docs/api/use-script-trigger-consent){lang="ts"} composable offers flexible interaction options suitable for various scenarios.
`useScriptConsent` is a superset of the deprecated `useScriptTriggerConsent`. Every option on the old composable works unchanged; see [migration](#migration-from-usescripttriggerconsent).

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 = useScriptConsent()
```

```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 = useScriptConsent({ 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, it stays resolved. Watch `consented` if a script needs 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 = useScriptConsent({
consent: agreedToCookies,
postConsentTrigger: () => new Promise<void>(resolve =>
setTimeout(resolve, 3000),
),
})
```

## Granular consent (Google Consent Mode v2)

Pass a `default` state and the composable tracks every category reactively. Registry scripts that declare a `consentAdapter` automatically subscribe when you pass the composable as their `consent`.

There may be instances where you want to trigger the script load after a certain event, only if the user has consented.
### GCMv2 category schema

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.
| Category | Purpose |
| --- | --- |
| `ad_storage` | Advertising cookies and storage |
| `ad_user_data` | Sending user data to ad platforms |
| `ad_personalization` | Personalised ads and retargeting |
| `analytics_storage` | Analytics cookies and storage |
| `functionality_storage` | Functional features like chat or preferences |
| `personalization_storage` | Personalised content |
| `security_storage` | Security features such as fraud prevention |

Each category is either `'granted'` or `'denied'`.

### Vendor mapping

Registry scripts map GCMv2 categories to their internal consent APIs via an adapter. Representative mappings:

| Script | Categories used |
| --- | --- |
| Google Tag Manager / Google Analytics | `ad_storage`, `ad_user_data`, `ad_personalization`, `analytics_storage` |
| Meta Pixel | `ad_storage`, `ad_user_data`, `ad_personalization` |
| TikTok Pixel | `ad_storage`, `ad_user_data` |
| X Pixel | `ad_storage`, `ad_user_data` |
| Reddit Pixel | `ad_storage` |
| Snapchat Pixel | `ad_storage`, `ad_user_data` |
| Hotjar | `analytics_storage` |
| Clarity | `analytics_storage` |
| Crisp / Intercom | `functionality_storage` |

Refer to each script's registry page for the exact mapping. Scripts without a declared adapter only observe the binary load gate.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

### Example

```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 = useScriptConsent({
default: {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
},
})

useScriptGoogleTagManager({
id: 'GTM-XXXXXX',
scriptOptions: { consent },
})

useScriptMetaPixel({
id: '1234567890',
scriptOptions: { consent },
})

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',
})
}
```

`consent.update()`{lang="ts"} merges the partial state and fans out to every subscribed adapter once per tick, so calling it three times in a row fires one update per script.

## Third-party CMP recipes

When a dedicated Consent Management Platform owns the UI, bridge its events into `useScriptConsent.update()`{lang="ts"}.

### OneTrust

```ts
const consent = useScriptConsent({
default: {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
},
})

onNuxtReady(() => {
function apply() {
const groups = (window as any).OnetrustActiveGroups as string | undefined
if (!groups)
return
consent.update({
analytics_storage: groups.includes('C0002') ? 'granted' : 'denied',
ad_storage: groups.includes('C0004') ? 'granted' : 'denied',
ad_user_data: groups.includes('C0004') ? 'granted' : 'denied',
ad_personalization: groups.includes('C0004') ? 'granted' : 'denied',
})
}

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

### Cookiebot

```ts
const consent = useScriptConsent({
default: {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
},
})

onNuxtReady(() => {
function apply() {
const cb = (window as any).Cookiebot
if (!cb?.consent)
return
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',
})
}

apply()
window.addEventListener('CookiebotOnAccept', apply)
window.addEventListener('CookiebotOnDecline', apply)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

## Migration from `useScriptTriggerConsent`

`useScriptTriggerConsent` is deprecated and logs a dev-only warning. It re-exports `useScriptConsent`, so options are 100 percent compatible: rename the function, keep everything else.

**Before**

```ts
const consent = useScriptTriggerConsent({
consent: agreedToCookies,
postConsentTrigger: 'onNuxtReady',
})

useScript('https://www.google-analytics.com/analytics.js', { trigger: consent })
```

**After**

```ts
const consent = useScriptConsent({
consent: agreedToCookies,
postConsentTrigger: 'onNuxtReady',
})

useScript('https://www.google-analytics.com/analytics.js', { trigger: consent })
```

Moving to granular state is purely additive: add a `default` object and swap `trigger: consent` for `scriptOptions: { consent }` on registry scripts that declare a `consentAdapter`.
10 changes: 4 additions & 6 deletions docs/content/scripts/bing-uet.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,12 @@ 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). Use `defaultConsent` to set the default state before the script loads. If consent is denied, UET only sends anonymous data.

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

function grantConsent() {
Expand All @@ -76,3 +72,5 @@ function grantConsent() {
}
</script>
```

You can still use `onBeforeUetStart` for any other pre-load setup.
34 changes: 34 additions & 0 deletions docs/content/scripts/clarity.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,37 @@ links:

::script-types
::

## Consent Mode

Clarity supports a cookie consent toggle (boolean) or an advanced consent vector (record). Use `defaultConsent` to set the state before Clarity starts:

```ts
useScriptClarity({
id: 'YOUR_PROJECT_ID',
defaultConsent: false, // disable cookies until user opts in
})

// or advanced vector
useScriptClarity({
id: 'YOUR_PROJECT_ID',
defaultConsent: {
ad_storage: 'denied',
analytics_storage: 'granted',
},
})
```

To update consent at runtime:

```vue
<script setup lang="ts">
const { proxy } = useScriptClarity()

function acceptAnalytics() {
proxy.clarity('consent', true)
}
</script>
```

See [Clarity cookie consent](https://learn.microsoft.com/en-us/clarity/setup-and-installation/cookie-consent) for details.
Loading
Loading