@nuxtlib/consent
v1.0.4
Published
Nuxt cookie consent module with composable, banner, and optional provider sync helpers
Maintainers
Readme
@nuxtlib/consent
For Nuxt teams that want free consent banner:
- Optimized for performance
- That only shows for real users after interaction signals
- That persists consent cookies
- With optional Nuxt i18n integration
- With optional Google Analytics consent updates
This module is optimized to stay lightweight in real apps:
- Consent UI is shown only for real users after interaction signals (click, touch, key, scroll, pointer movement threshold).
- The dialog is lazy-rendered through
<CookiesConsent />, and that public wrapper is registered client-only by the module. - Interaction listeners are cleaned up after the user is confirmed as real, avoiding long-lived event overhead.
Search terms: nuxt cookie consent, gdpr banner nuxt, google consent mode, nuxt scripts consent, tailwind v4 nuxt module.
Navigation
- Quickstart
- Public API
- Supported Integrations
- Localization
- How to Configure Google Analytics
- Testing
- Project Customization Map
- Troubleshooting
- Roadmap
- Usage Cases
Quickstart
Copy/paste minimal setup:
This quick setup gives you the consent UI and cookie persistence only. Provider integrations such as Google Analytics are optional and must be enabled separately.
Install dependencies:
npm install -D @nuxtlib/consent tailwindcss @tailwindcss/viteassets/css/main.css:
@import "tailwindcss";
@import "@nuxtlib/consent/tailwind";nuxt.config.ts:
import tailwindcss from '@tailwindcss/vite'
export default defineNuxtConfig({
modules: ['@nuxtlib/consent'],
css: ['~/assets/css/main.css'],
vite: { plugins: [tailwindcss()] },
nuxtlibConsent: {
cookiePrefix: 'myapp_name', // cookie key namespace
cookieSecure: true, // OPTIONAL, default true, set to false when you want to test on localhost
cookieMaxAge: 60 * 60 * 24 * 365, // OPTIONAL - default 1 year, in seconds
locale: 'pl', // used only when the app does not use Nuxt i18n - defaults to 'en'
},
})app.vue:
<script setup lang="ts">
const consentRef = ref<{ toggleConsentVisibility: () => void } | null>(null)
</script>
<template>
<button @click="consentRef?.toggleConsentVisibility()">
Cookie settings
</button>
<CookiesConsent ref="consentRef" />
</template>What you should see:
- The modal appears only after a real-user interaction such as click, touch, key press, scroll, wheel, or meaningful pointer movement.
- Consent state is stored in
<prefix>_decidedand<prefix>_consent. - No provider integrations run unless you enable them explicitly.
Notes:
- You do not need to wrap
<CookiesConsent />in<ClientOnly>. The module already registers it as client-only. useConsent()is SSR-safe. When consent cookies are missing, server renders use fallback values without writing default consent cookies during SSR.
Use cookieSecure: false only for local HTTP testing. Keep cookieSecure: true in production HTTPS.
If you want provider sync, see Supported Integrations and How to Configure Google Analytics.
Public API
<CookiesConsent />
Mount this once near the app root. The component exposes:
showDeclineButton: shows a "decline all" action button whentrueenableBackground: adds a dark blurred backdrop behind the modal whentruetoggleConsentVisibility()to manually reopen or close the consent modal.
Implementation notes:
- The module registers
<CookiesConsent />as client-only, so consumers do not need an extra<ClientOnly>wrapper. - This keeps the consent UI out of SSR while preserving the same public component API.
Example:
<CookiesConsent
:show-decline-button="true"
:enable-background="true"
/>useConsent()
Auto-imported composable for consent state and actions:
consent: consent category state backed by cookies when presentdecided: first-decision flag backed by cookies when presentsetConsent(patch): partial update for one or more categoriesacceptAll(): enables all categoriesdeclineAll(): disables all categories
SSR behavior:
- When consent cookies are missing,
useConsent()returns normalized fallback values during SSR. - Those fallback values are not written back as
Set-Cookieheaders during server render finalization.
Current consent categories:
analyticsadspersonalizationfunctional
useIsRealUser(options?)
Auto-imported composable that confirms real interaction before the consent UI is shown automatically.
Current option:
movementThresholdPxwith a default of8
Supported Integrations
Current built-in integrations:
googleAnalytics: Google Analytics consent mode sync through@nuxt/scripts
Localization
Built-in locales:
endepl
Without @nuxtjs/i18n, the module uses nuxtlibConsent.locale and falls back to en.
export default defineNuxtConfig({
nuxtlibConsent: {
locale: 'pl',
},
})With @nuxtjs/i18n, the module registers its bundled en, de, and pl translations through the i18n:registerModule hook and resolves consent copy from the nuxtlibConsent namespace in your app i18n instance.
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n', '@nuxtlib/consent'],
i18n: {
defaultLocale: 'en',
locales: ['en', 'de', 'pl'],
},
nuxtlibConsent: {
locale: 'en', // fallback only when the app does not use Nuxt i18n
},
})App i18n always wins over nuxtlibConsent.locale. If you add your own locale, provide consent strings under the nuxtlibConsent namespace.
You can also override the module's bundled locales from the consuming app. For example, if your app already has i18n/locales/en.ts, i18n/locales/de.ts, or i18n/locales/pl.ts, you can replace some keys or the entire nuxtlibConsent namespace there.
// i18n/locales/en.ts
export default {
nuxtlibConsent: {
title: 'Your custom title',
buttons: {
acceptAll: 'Your custom button label',
},
},
}// i18n/locales/fr.ts
export default {
nuxtlibConsent: {
title: 'Nous utilisons des cookies',
description: 'Nous utilisons des cookies et des technologies similaires pour faire fonctionner le site, mesurer son utilisation et, avec votre permission, personnaliser le contenu et la publicite.',
categories: {
analytics: {
label: 'Analytiques',
description: '...',
},
ads: {
label: 'Publicite / marketing',
description: '...',
},
personalization: {
label: 'Personnalisation',
description: '...',
},
functional: {
label: 'Fonctionnels (preferences)',
description: '...',
},
},
buttons: {
acceptAll: 'Tout accepter',
customize: 'Personnaliser',
hideSettings: 'Masquer les parametres',
save: 'Enregistrer les parametres',
},
},
}Notes:
- You only need to provide the
nuxtlibConsentnamespace in app locale files. - The consuming app can override bundled
en,de, andpllocales partially or completely by definingnuxtlibConsentin its own locale files. - If you override the full namespace for a locale, provide every key your UI needs.
- If you override only part of the namespace, make sure your app i18n fallback config covers the remaining keys.
- Without Nuxt i18n, only the bundled
en,de, andpllocales are available.
How to Configure Google Analytics
GA sync is optional and is only loaded when you enable the integration explicitly.
export default defineNuxtConfig({
modules: ['@nuxtlib/consent', '@nuxt/scripts'],
scripts: {
registry: {
googleAnalytics: { id: 'G-XXXXXXXXXX' },
},
},
nuxtlibConsent: {
integrations: {
googleAnalytics: true,
},
},
})- Install and enable
@nuxt/scripts. - Add the GA measurement ID in
scripts.registry.googleAnalytics.id. - Enable
nuxtlibConsent.integrations.googleAnalytics.
Where to get the GA ID:
- Open Google Analytics (GA4) for your property.
- Go to
Admin->Data Streams-> open your Web stream. - Copy the
Measurement ID(formatG-XXXXXXXXXX).
Behavior:
- On GA load, sends
gtag('consent', 'default', deniedState). - Watches consent cookie state.
- Sends
gtag('consent', 'update', mappedState)on changes.
If nuxtlibConsent.integrations.googleAnalytics is not enabled, this plugin is not added to the app at all.
For advanced script loading/configuration options, read the Nuxt Scripts docs: https://scripts.nuxt.com/.
Testing
Recommended test path:
- Open your app in a fresh session (incognito or cleared cookies).
- Trigger real interaction (click/scroll) and confirm banner appears.
- Choose consent options and verify cookies
<prefix>_decidedand<prefix>_consent. - Reopen settings through
toggleConsentVisibility()and confirm the modal reflects the saved cookie state. - If
integrations.googleAnalyticsis enabled, verify analytics/debug behavior with one of these methods:
Method A: Google Tag Manager Preview / Tag Assistant (recommended when GTM is in your stack)
- Start GTM Preview for your site.
- Interact with the site and consent banner.
- Verify consent-related calls and page events in the preview timeline.
Method B: Chrome extension + GA DebugView
- Install and enable
Google Analytics DebuggerChrome extension. - Visit your app and trigger page/navigation events after consent.
- In GA4, open
DebugViewand confirm events appear from your debug session.
Tip:
- Test both first-load flow and "reopen settings" flow via
toggleConsentVisibility().
Project Customization Map
src/module.ts: module wiring (runtime config, auto-imports, component registration, and conditional plugin injection).src/integrations.ts: supported integration registry and validation rules.src/runtime/composables/useConsent.ts: consent categories + cookie persistence rules.src/runtime/composables/useConsentI18n.ts: consent text resolution with optional app i18nsrc/runtime/lang/en.ts: bundled English consent copysrc/runtime/lang/pl.ts: bundled Polish consent copysrc/runtime/composables/useIsRealUser.ts: real-user interaction detection strategy.src/runtime/components/CookiesConsent.vue: public wrapper component used in apps (<CookiesConsent />).src/runtime/components/ConsentModal.client.vue: modal UI copy, buttons, and category controls.src/runtime/components/BaseSwitch.vue: switch control used in modal category toggles.src/runtime/plugins/ga.client.ts: optional GA consent mapping and update calls.src/runtime/tailwind.css: Tailwind source export used via@nuxtlib/consent/tailwind.src/runtime/augments.app.d.ts: type augmentation forruntimeConfig.public.nuxtlibConsent.
Troubleshooting
Consent banner never appears:
- Confirm you mounted
<CookiesConsent />. - Interact with the page first (click/scroll/key/touch); wrapper waits for a real-user signal.
Do I need <ClientOnly>?
- No. The module already registers
<CookiesConsent />as client-only. - You only need
ClientOnlyif you are wrapping your own custom consent UI outside the shipped component.
Seeing SSR / SWR header-write errors:
- Update to a version that includes the SSR-safe
useConsent()change. - The module now reads fallback consent state during SSR without emitting default
Set-Cookieheaders.
Cookies not persisting on localhost:
- Set
cookieSecure: falsewhile testing onhttp://localhost. - Revert to
cookieSecure: truefor production HTTPS.
Consent UI is unstyled:
- Confirm Tailwind v4 is installed.
- Confirm Vite plugin
@tailwindcss/viteis enabled. - Confirm your CSS imports
@nuxtlib/consent/tailwind.
Translations are not switching:
- Confirm
@nuxtjs/i18nis installed and enabled. - Put app overrides under the
nuxtlibConsentnamespace. - If you provide partial overrides, confirm your app i18n fallback config is active.
GA consent updates missing:
- Confirm
nuxtlibConsent.integrations.googleAnalyticsis enabled. - Confirm
@nuxt/scriptsis installed and enabled. - Confirm
scripts.registry.googleAnalytics.idis configured.
Roadmap
Opt out RealUsers only
Add a module option to disable real-user gating when teams need immediate banner rendering for all visitors (for strict legal/compliance flows), while keeping current behavior as default. Planned output: one explicit config flag and documented behavior matrix.
More provider integrations
Extend the integration registry beyond Google Analytics so teams can opt into additional consent targets through the same explicit nuxtlibConsent.integrations interface. Planned output: more provider plugins, docs, and migration notes.
Opt in show decline all button
Add a config flag to explicitly enable/disable a visible "Decline all" action in the modal so product/legal teams can align UI behavior with policy requirements. Planned output: UI config option plus accessibility and copy recommendations.
Usage Cases
Use the saved consent state anywhere in your app through useConsent().
Only enable a feature after the required consent was granted:
<script setup lang="ts">
const { consent } = useConsent()
const canRunAnalyticsFeature = computed(() => consent.value.analytics)
</script>
<template>
<AnalyticsDashboard v-if="canRunAnalyticsFeature" />
<p v-else>
This feature is available only after analytics consent is accepted.
</p>
</template>Only initialize a third-party tool when consent changes to allowed:
const { consent } = useConsent()
watch(
() => consent.value.functional,
(allowed) => {
if (!allowed || !import.meta.client) {
return
}
startSupportChat()
},
{ immediate: true },
)Typical mappings in real apps:
analytics: analytics SDKs, heatmaps, product metrics, A/B testing toolsads: ad pixels, retargeting scripts, conversion trackingpersonalization: recommendation engines, personalized content, ad personalizationfunctional: support chat, saved UI preferences, embedded tools that are not strictly essential
This pattern works well when some parts of the app should stay hidden, disabled, or not initialized until the user has explicitly granted the matching consent category.
You can also use <ConsentModal /> directly instead of <CookiesConsent /> when you want full control over when the modal appears.
That is useful when:
- you do not want the built-in real-user gating from
<CookiesConsent /> - you want to control the backdrop, placement, transitions, or surrounding layout yourself
Example:
<script setup lang="ts">
const consentModalRef = ref<{ syncFromCookies: () => void } | null>(null)
const showConsentModal = ref(false)
function openConsentModal() {
consentModalRef.value?.syncFromCookies()
showConsentModal.value = true
}
</script>
<template>
<button @click="openConsentModal()">
Privacy settings
</button>
<ConsentModal
v-if="showConsentModal"
ref="consentModalRef"
@decision-has-been-made="showConsentModal = false"
/>
</template><CookiesConsent /> is still the recommended default because it already handles the first-visit flow, real-user detection, lazy rendering, and reopen behavior. If you use <ConsentModal /> directly, you are responsible for deciding when to show it and when to hide it.
