Google Tag Manager
Google Tag Manager loads tags from a web container, letting you change tracking configuration without redeploying the app.
GTM can load any tags configured in your container, so its performance cost varies with that configuration. If you only need Google Analytics, the useScriptGoogleAnalytics() composable may be simpler.
Nuxt Scripts only loads the GTM container. Tracking comes from the tags and triggers in your GTM workspace or from your own dataLayer.push calls. For automatic page, click, scroll, and video tracking, enable GA4 Enhanced Measurement for the GA4 web data stream.
Nuxt Config Setup
Add this to your nuxt.config.ts to load Google Tag Manager globally. Alternatively you can use the useScriptGoogleTagManager composable for more control.
export default defineNuxtConfig({
scripts: {
registry: {
googleTagManager: {
id: 'GTM-XXXXXX',
trigger: 'onNuxtReady',
}
}
}
})This config automatically enables first-party mode (bundle). See below to customise.
useScriptGoogleTagManager()
The useScriptGoogleTagManager composable lets you have fine-grain control over when and how Google Tag Manager is loaded on your site.
const { proxy } = useScriptGoogleTagManager()
proxy.dataLayer.push({ event: 'conversion', value: 1 })Please follow the Registry Scripts guide to learn more about advanced usage.
First-Party Mode: Privacy Focused Proxy
No extra config needed. The script is bundled and served from your domain instead of a third-party CDN, eliminating an extra DNS lookup and improving load times. Learn more.
export default defineNuxtConfig({
scripts: {
// ✅ First-party mode: bundled
registry: {
googleTagManager: {
id: 'GTM-XXXXXX',
trigger: 'onNuxtReady',
},
},
},
})Example
Using Google Tag Manager in a component.
<script setup lang="ts">
const { proxy } = useScriptGoogleTagManager()
// noop in development, ssr
// just works in production, client
function handleAction() {
proxy.dataLayer.push({ event: 'conversion', value: 1 })
}
</script>
<template>
<div>
<button @click="handleAction">
Send Event
</button>
</div>
</template>Sending page events
Use the proxy with useScriptEventPage() to push an event after Nuxt finishes the initial client render or a later route change and updates the page title:
const { proxy } = useScriptGoogleTagManager({
id: 'YOUR_ID' // id is only needed if you haven't configured globally
})
useScriptEventPage(({ title, path }) => {
// Runs for the initial render when registered early, then for route changes.
proxy.dataLayer.push({
event: 'pageview',
title,
path
})
})
Consent Mode
Google Tag Manager accepts GCMv2 consent state. defaultConsent enters the queue before the gtm.js event; use consent.update() for later choices. If client state determines the initial default, resolve it before calling the composable and pass it through defaultConsent. A later consent.default() call queues another default after initialization and cannot reproduce the original ordering.
Open the cookie consent, granular consent, or regional consent example on StackBlitz.
Consent Mode v2 signals
| Signal | Purpose |
|---|---|
ad_storage | Cookies for advertising |
ad_user_data | Send user data to Google for ads |
ad_personalization | Personalized ads (remarketing) |
analytics_storage | Cookies for analytics |
Example
<script setup lang="ts">
const { proxy, consent } = useScriptGoogleTagManager({
id: 'GTM-XXXXXX',
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',
})
}
useScriptEventPage(({ title, path }) => {
proxy.dataLayer.push({ event: 'pageview', title, path })
})
</script>
Per-region defaults
Pass an array to defaultConsent to queue one consent-default command per entry, in order. This matches Google's region-specific consent pattern: more specific regions (e.g. US-CA) override broader ones (US); an entry with no region is the unscoped global fallback.
<script setup lang="ts">
useScriptGoogleTagManager({
id: 'GTM-XXXXXX',
defaultConsent: [
{
// EEA + UK + Switzerland: start denied and wait 500ms for a choice.
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
region: ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'GB', 'IS', 'LI', 'NO', 'CH'],
wait_for_update: 500,
},
{
// Everywhere else: granted by default.
ad_storage: 'granted',
ad_user_data: 'granted',
ad_personalization: 'granted',
analytics_storage: 'granted',
},
],
})
</script>
The module forwards each entry verbatim, in input order. Precedence between region-scoped and unscoped defaults is enforced by gtag at runtime, not by ordering.
consent.update() and consent.default() both accept any Partial<ConsentState>; missing categories stay at their current value. Both methods validate input against the canonical GCMv2 schema and warn via consola on unknown keys or non-granted/denied values. onBeforeGtmStart remains available as a general escape hatch for any other pre-gtm.start setup (only when the GTM ID is passed directly to the composable, not via nuxt.config).
idstring required GTM container ID (format: GTM-XXXXXX)
lstring = 'dataLayer'Optional dataLayer variable name
authstringAuthentication token for environment-specific container versions
previewstringPreview environment name
cookiesWinboolean | 'x'debugboolean | 'x'Enables debug mode when true
npaboolean | '1'No Personal Advertising - disables advertising features when true
dataLayerstringenvNamestringEnvironment name for environment-specific container
authReferrerPolicystringdefaultConsentunknown | unknown[]Default GCMv2 consent state(s) fired as `['consent','default', state]` onto the dataLayer before the `gtm.js` event. Pass an array to fire multiple defaults — for example, different defaults per `region` (more specific regions override broader ones at runtime).
Examples
Server-side GTM
With server-side tagging, the web container still runs in the browser and sends measurement requests to a server container that you operate. Set each supported tag's transport URL (for example, server_container_url in a Google tag) to the server container; changing the GTM loader URL alone does not reroute those requests.
Prerequisites include a server-side GTM container, hosting such as Cloud Run or a manual deployment, and a custom domain.
Configuration
If your configured first-party tagging domain serves the web container loader, override the source and retain the container ID query parameter:
// nuxt.config.ts
export default defineNuxtConfig({
scripts: {
registry: {
googleTagManager: {
id: 'GTM-XXXXXX',
trigger: 'onNuxtReady',
scriptInput: {
src: 'https://analytics.example.com/gtm.js?id=GTM-XXXXXX'
}
}
}
}
})
This source override only changes where the browser loads gtm.js. Configure the web and server containers to route measurement requests to the server container, following Google's server-side tagging documentation.