Modules

Cookie Consent

GDPR cookie banner with categories other modules contribute to, consent gating hooks and proof of consent stored on the user.

A cookie consent banner with categories, a preferences dialog, helpers for gating scripts on consent, and consent that is stored on the signed-in user as proof.

Requires@frigear-nu/module-kit and @frigear-nu/ui (a hard dependency of the package; the components also need @nuxt/ui in the host). Optional@frigear-nu/users (with NuxtHub) to store the consent on the user, and @frigear-nu/auth, declared only so it is set up first. Without users, consent lives only in the cookie.

Design

  • Opt-in per category. necessary is always on; every other category is off until the visitor grants it. Rejecting is exactly as easy as accepting.
  • Specific and informed. Each category can list its cookies (name, provider, purpose, duration); they are shown in the preferences dialog.
  • Versioned. The consent version is your version option plus a hash of the category/cookie set. Adding a category or cookie (including from another module) asks everyone again. The old accepted/rejected cookie values are migrated.
  • Provable. For signed-in users the timestamp, version and granted categories are stored on the users row.

Quick setup

export default defineNuxtConfig({
  modules: ['@nuxt/ui', '@frigear-nu/cookie-consent'],
  cookieConsent: {
    version: '1', // bump to ask everyone again
    policyUrl: '/legal/gdpr',
  },
})
<!-- app.vue, inside <UApp> -->
<CookieConsentBanner />

Put <CookieConsentManage /> (a "Manage cookies" button) in the footer so visitors can change their mind. It opens the preferences dialog rendered by the banner.

Options

All options live under cookieConsent.

OptionTypeDefaultPurpose
cookieNamestring'cookie-consent'Name of the consent cookie.
maxAgenumber15552000 (180 days)Cookie lifetime in seconds.
versionstring'1'Manual version; changing it asks every visitor again. The category set is hashed into the final version.
categoriesConsentCategory[]necessary (required), analytics, marketingCategories to show. Other modules add more with extendCookieConsent.
excludeRoutesstring[]['/authorize']Exact routes where the banner is hidden.
collapseOnScrollboolean | numbertrueCollapse the banner into a round button after this many px scrolled (true = 80, false = never).
policyUrlTranslatednoneLink to the cookie/privacy policy shown in the banner.
policyLabelTranslatedi18n key cookieConsent.policy ("Read more")Text of the policy link; wins over the translation.
persistToUserbooleantrue when @frigear-nu/users is installedStore the consent on the signed-in user. Forced off without users.
iconstring'lucide:cookie'Iconify name (collection:name) of the badge and collapsed button icon.

Translated is a string or a map of locale code to value ({ da, en }). A locale without an entry falls back to its base language, then en, then the first entry.

export default defineNuxtConfig({
  cookieConsent: {
    policyUrl: { en: '/legal/gdpr', da: '/da/juridisk/gdpr' },
    policyLabel: { en: 'Read our cookie policy', da: 'Læs vores cookiepolitik' },
    categories: [
      { key: 'necessary', required: true, label: { en: 'Necessary', da: 'Nødvendige' } },
      { key: 'analytics', label: 'Analytics', cookies: [{ name: '_ga', provider: 'Google', duration: '2 years' }] },
    ],
  },
})

The icon is added to Nuxt Icon's client bundle by the module (through the icon:clientBundleIcons hook), because Nuxt Icon does not scan node_modules; no icon.clientBundle.icons entry is needed. Icons from custom collections work as long as the collection is configured in Nuxt Icon.

Types

interface ConsentCookie {
  name: string
  provider?: string
  purpose?: Translated
  duration?: string // human readable, e.g. '1 year' or 'Session'
}

interface ConsentCategory {
  key: string
  required?: boolean // always on and not switchable
  defaultEnabled?: boolean // pre-selected in the preferences dialog; never applied without the visitor saving. Default false
  label?: Translated // default: i18n key cookieConsent.categories.<key>.label
  description?: Translated // default: i18n key cookieConsent.categories.<key>.description
  cookies?: ConsentCookie[]
}

/** What is stored in the consent cookie and, for signed-in users, in the database. */
interface ConsentState {
  v: string // consent version the choice was made against
  ts: number // epoch ms
  categories: Record<string, boolean>
}

Server

getCookieConsent(event: H3Event): ConsentState | null // null = undecided or outdated
hasCookieConsent(event: H3Event, category: string): boolean // gate tracking in SSR / API code
getCookieConsentConfig(event: H3Event): PublicModuleOptions | undefined

getCookieConsent and hasCookieConsent are auto-imported in server code.

App

Composables

const {
  config, categories, consent, hasResponded, isAllowed,
  acceptAll, rejectAll, save, reset,
  isPreferencesOpen, openPreferences, hydrateFromUser,
} = useCookieConsent()

isAllowed(key: string): boolean
save(choices: Record<string, boolean>): Promise<ConsentState> // required categories are always granted
reset(): Promise<void> // withdraws consent: only required categories remain and the banner shows again

// Runs now if already granted, or as soon as it is; the returned function runs on withdrawal.
onCookieConsent(category: string, callback: () => (() => void) | undefined): () => void // returns a stop function

useConsentT() // translated texts of the module
onCookieConsent('analytics', () => {
  const stop = startTracking()
  return () => stop()
})

Components

The default components are registered with the lowest priority. A component with the same name in your app (or an extended layer) wins, with no module option, including where other module components use it.

ComponentReplaces
CookieConsentBannerThe whole banner, including the collapsed round button
CookieConsentBannerActionsOnly the button row of the banner (preferences / reject / accept)
CookieConsentPreferencesThe preferences dialog
CookieConsentManageThe "Manage cookies" footer button

CookieConsentBanner renders CookieConsentBannerActions and CookieConsentPreferences itself, so replacing one part keeps the defaults of the others. A replacement banner builds on useCookieConsent() and should call hydrateFromUser() and the cookie-consent:ready hook on mount, as the default one does. Copy a default from packages/cookie-consent/src/runtime/app/components as a starting point.

Contributing categories (from a module)

Other modules add categories or cookies during setup(); they are merged by key, in any order, after every module has set up (earlier labels win, cookies are concatenated, required is sticky).

import { extendCookieConsent, defineConsentCategory } from '@frigear-nu/cookie-consent/utils'

extendCookieConsent(nuxt, {
  categories: [
    defineConsentCategory({ key: 'necessary', required: true, cookies: [{ name: 'stripe_mid', provider: 'Stripe', duration: '1 year', purpose: 'Fraud prevention' }] }),
  ],
})

Utils

@frigear-nu/cookie-consent/utils is framework-agnostic and safe in a module's setup(), server code and tests.

ExportPurpose
extendCookieConsent(nuxt, { categories })Contribute categories; calls the cookie-consent:extend hook.
defineConsentCategory(category)Identity helper for typing a category.
DEFAULT_CATEGORIESnecessary, analytics, marketing.
mergeCategories(...lists)Merge category lists by key.
computeVersion(manual, categories)Final consent version (manual version plus category hash).
buildConsent(categories, version, choices, now?)Build a ConsentState; required categories granted, unknown keys dropped.
normalizeConsent(raw, categories, version, now?)Cookie value (including legacy strings) to a valid state, or null when the visitor must be asked again.
acceptAllChoices / rejectAllChoicesChoice maps for accept all / reject all.
localize(value, locale, fallback?)Resolve a Translated value.

Types ConsentCategory, ConsentCookie, ConsentState and Translated are exported from the same path.

Hooks

HookWhereWhenPayload
cookie-consent:extendNuxt (build)extendCookieConsent registers categories(categories: ConsentCategory[])
cookie-consent:resolvedNuxt (build)Once, after every module has set up(categories: ConsentCategory[]); mutate to adjust, the version is derived afterwards
cookie-consent:changeNuxt app (client)Every save, accept, reject or withdrawal, and when a stored choice is picked up from the user(state: ConsentState | null, previous: ConsentState | null); state is null on withdrawal
cookie-consent:readyNuxt app (client)Banner mounted(state: ConsentState | null); null while undecided
cookie-consent:savedNitroA signed-in user's consent was stored({ userId: number | string, state: ConsentState, acceptedAt: number })

Routes

Registered only with persistToUser enabled, which requires @frigear-nu/users.

MethodPathAuthPurpose
GET/api/cookie-consentSession onlyThe signed-in user's stored consent, { consent: ConsentState | null }. A returning user without a cookie on a new device gets their choice back (unless the version changed).
POST/api/cookie-consentSession onlyStore { categories, version }. 409 for an outdated version, 400 for unknown categories. The server sets the timestamp and forces required categories. Calls cookie-consent:saved.

Database

With persistToUser the module extends users (SQLite schema):

ColumnMeaning
cookie_consent_accepted_atEpoch ms of the latest response
cookie_consent_versionConsent version answered against
cookie_consent_categoriesJSON of what was granted

extendDatabaseTable only changes the generated schema, so generate and commit migrations in every consuming app and fixture. See Migrations.

Translations

Namespace cookieConsent, shipped in da and en and contributed to @nuxtjs/i18n (see Module Kit). Without @nuxtjs/i18n the components fall back to English. Override any string in your app's i18n/locales/<code>.json (see overriding module strings).

KeyUsed for
cookieConsent.description, accept, reject, preferences, expand, manage, saveBanner and buttons
cookieConsent.preferencesTitle, preferencesDescription, required, cookiesPreferences dialog
cookieConsent.policyPolicy link text when policyLabel is not set
cookieConsent.categories.<key>.label / .descriptionCategory texts; custom categories use the same keys, or set label / description in the config
Copyright © 2026