Features

Cookie Consent

Ask visitors before your app sets non-essential cookies, load scripts only after they agree, and keep proof.

Cookie Consent shows a banner that asks visitors which kinds of cookies they accept, and lets them change their mind later. Your code loads analytics or marketing scripts only after the visitor agrees. For signed-in users, the choice is also stored on their account as proof of consent. Use it when your app sets cookies that are not strictly necessary. For other per-user settings, such as colour mode, use Preferences instead.

The package is @frigear-nu/cookie-consent. It follows the General Data Protection Regulation (GDPR): each category except necessary stays off until granted, and rejecting is as easy as accepting.

What you get

  • Components (on by default): the banner with a preferences dialog, and a "Manage cookies" button for your footer. Replace any of them with your own.
  • Composables and helpers (on by default): useCookieConsent() and onCookieConsent() in the app, and hasCookieConsent() on the server.
  • Categories from other modules (on by default): a module that sets cookies adds its own category or cookies, and they appear in the dialog.
  • Proof of consent (on by default with Users): the time, version and granted categories are stored on the user, through two server routes.
  • Pages and emails. None.
ServerClientMobile
Installed for you: Module Kit. You add:@nuxt/ui, which the components need. Optional:Users with @nuxthub/core, to store the choice on the user. Without Users the choice lives only in the cookie.

Quick setup

pnpm add @frigear-nu/cookie-consent

Add it to nuxt.config.ts

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/ui', '@frigear-nu/cookie-consent'],
  cookieConsent: {
    version: '1', // bump to ask everyone again
    policyUrl: '/legal/gdpr',
  },
})

Render the banner

Put the banner inside <UApp>. Put <CookieConsentManage /> in your footer, so visitors can change their mind.

app.vue
<UApp>
  <NuxtPage />
  <CookieConsentBanner />
</UApp>

Generate the migration

With Users installed, every write to the users table fails until you add this migration. Without Users, skip this step.

The module adds three columns to users. Generate the migration and commit it:

pnpm db:generate
What you should see: on the first visit, the banner. After Accept or Reject, it collapses, and the cookie-consent cookie holds the choice.

Options

Options go under the cookieConsent key.

OptionTypeDefaultPurpose
cookieNamestring'cookie-consent'The name of the consent cookie.
maxAgenumber15552000 (180 days)The cookie's lifetime in seconds.
versionstring'1'Your version of the consent. Changing it asks every visitor again.
categoriesConsentCategory[]necessary, analytics, marketingThe categories to show.
excludeRoutesstring[]['/authorize']Exact routes where the banner is hidden.
collapseOnScrollboolean | numbertrueCollapses the banner into a round button after this many pixels of scrolling (true is 80).
policyUrlTranslatednoneThe link to your cookie or privacy policy.
policyLabelTranslated"Read more"The text of the policy link.
persistToUserbooleantrue with UsersStores the choice on the signed-in user. Always off without Users.
iconstring'lucide:cookie'The Iconify name (collection:name) of the badge and the collapsed button.

version is combined with a hash of the categories and their cookies. So adding a category or a cookie, from any module, asks everyone again. Old accepted and rejected cookie values are migrated.

categories replaces the default list, in which necessary is required: always on and not switchable. Other modules add categories with extendCookieConsent. Each category can list its cookies, which the preferences dialog shows.

policyLabel wins over the cookieConsent.policy translation.

icon is added to Nuxt Icon's client bundle for you. An icon from a custom collection needs that collection configured in Nuxt Icon.

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

// nuxt.config.ts
cookieConsent: {
  policyUrl: { en: '/legal/gdpr', da: '/da/juridisk/gdpr' },
  categories: [
    { key: 'necessary', required: true, label: { en: 'Necessary', da: 'Nødvendige' } },
    { key: 'analytics', label: 'Analytics', cookies: [{ name: '_ga', provider: 'Google', duration: '2 years' }] },
  ],
}
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

Check consent in server code before you track anything. Both helpers are auto-imported.

getCookieConsent(event: H3Event): ConsentState | null // null = undecided or outdated
hasCookieConsent(event: H3Event, category: string): boolean

App

Start a script once its category is granted. This example also stops tracking when the visitor withdraws:

onCookieConsent('analytics', () => {
  const stop = startTracking()
  return () => stop()
})

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

Components

A component with the same name in your app, or in a layer it extends, replaces the default everywhere. No option is needed.

ComponentWhat it renders
CookieConsentBannerThe whole banner, with the collapsed round button.
CookieConsentBannerActionsThe banner's button row: preferences, reject and accept.
CookieConsentPreferencesThe preferences dialog.
CookieConsentManageThe "Manage cookies" button.

The banner renders the button row and the dialog itself, so replacing one part keeps the others. A replacement banner calls hydrateFromUser() and the cookie-consent:ready hook on mount, as the default does. Start from a copy in packages/cookie-consent/src/runtime/app/components.

Add a category from a module

A module that sets cookies adds them from setup(). Lists are merged by key after every module has set up. The first label wins, cookies are concatenated, and required stays on once set.

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 safe in a module's setup(), server code and tests. It also exports the types.

ExportPurpose
extendCookieConsent(nuxt, { categories })Adds categories, and calls the cookie-consent:extend hook.
defineConsentCategory(category)Types a category.
DEFAULT_CATEGORIESnecessary, analytics and marketing.
mergeCategories(...lists)Merges category lists by key.
computeVersion(manual, categories)The final consent version: your version plus the category hash.
buildConsent(categories, version, choices, now?)Builds a ConsentState. Grants required categories and drops unknown keys.
normalizeConsent(raw, categories, version, now?)A valid state from a cookie value, or null when the visitor must be asked again.
acceptAllChoices / rejectAllChoicesThe choices for accept all and reject all.
localize(value, locale, fallback?)Resolves a Translated value.

Hooks

Cookie Consent calls these build-time, app and Nitro hooks.

HookWhereWhenPayload
cookie-consent:extendNuxt (build)extendCookieConsent adds categories.(categories: ConsentCategory[])
cookie-consent:resolvedNuxt (build)Once, after every module has set up. Change the list here.(categories: ConsentCategory[])
cookie-consent:changeNuxt app (client)Every save, accept, reject or withdrawal, and when a stored choice is loaded from the user.(state, previous), both ConsentState | null. state is null on withdrawal.
cookie-consent:readyNuxt app (client)The banner is mounted.(state: ConsentState | null), null while undecided.
cookie-consent:savedNitroA signed-in user's choice was stored.({ userId: number | string, state: ConsentState, acceptedAt: number })

Routes

Routes the module registers, and who may call them. They exist only with persistToUser, so only with Users. The path is cookieConsentApi.root from @frigear-nu/cookie-consent/api. Both routes are session only.

MethodPathAuthPurpose
GET/api/cookie-consentsession onlyThe signed-in user's stored choice: { consent: ConsentState | null }.
POST/api/cookie-consentsession onlyStores { categories, version }. Answers 200 with { consent }.

GET gives a returning user on a new device their choice back, unless the version changed.

POST sets the timestamp on the server and always grants required categories. It then calls cookie-consent:saved. It answers 409 outdated_version for an old version and 400 unknown_categories for unknown categories.

Database

Columns the module adds to users, only with persistToUser. Generate and commit a migration after installing. See Database extensions.

ColumnHolds
cookie_consent_accepted_atThe time of the latest choice, in epoch milliseconds.
cookie_consent_versionThe consent version the choice was made against.
cookie_consent_categoriesJSON of what was granted.

Translations

Override any string in your app's i18n/locales/<code>.json. The module registers da and en under cookieConsent. Without @nuxtjs/i18n the components use English. See overriding module strings.

KeyUsed for
cookieConsent.description, accept, reject, preferences, expand, manage, saveThe banner and its buttons.
cookieConsent.preferencesTitle, preferencesDescription, required, cookiesThe preferences dialog.
cookieConsent.policyThe policy link, when policyLabel is not set.
cookieConsent.categories.<key>.label and .descriptionCategory texts. Custom categories use the same keys, or label and description in the config.

Next steps

  • Users – install it to keep proof of consent on each user.
  • Database extensions – how the consent columns join the users table.
  • Extension points – how modules add categories without importing each other.
Copyright © 2026