Cookie Consent
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()andonCookieConsent()in the app, andhasCookieConsent()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.
@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
Install Cookie Consent
pnpm add @frigear-nu/cookie-consent
Add it to 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.
<UApp>
<NuxtPage />
<CookieConsentBanner />
</UApp>
Generate the migration
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
cookie-consent cookie holds the choice.Options
Options go under the cookieConsent key.
| Option | Type | Default | Purpose |
|---|---|---|---|
cookieName | string | 'cookie-consent' | The name of the consent cookie. |
maxAge | number | 15552000 (180 days) | The cookie's lifetime in seconds. |
version | string | '1' | Your version of the consent. Changing it asks every visitor again. |
categories | ConsentCategory[] | necessary, analytics, marketing | The categories to show. |
excludeRoutes | string[] | ['/authorize'] | Exact routes where the banner is hidden. |
collapseOnScroll | boolean | number | true | Collapses the banner into a round button after this many pixels of scrolling (true is 80). |
policyUrl | Translated | none | The link to your cookie or privacy policy. |
policyLabel | Translated | "Read more" | The text of the policy link. |
persistToUser | boolean | true with Users | Stores the choice on the signed-in user. Always off without Users. |
icon | string | '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.
| Component | What it renders |
|---|---|
CookieConsentBanner | The whole banner, with the collapsed round button. |
CookieConsentBannerActions | The banner's button row: preferences, reject and accept. |
CookieConsentPreferences | The preferences dialog. |
CookieConsentManage | The "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.
| Export | Purpose |
|---|---|
extendCookieConsent(nuxt, { categories }) | Adds categories, and calls the cookie-consent:extend hook. |
defineConsentCategory(category) | Types a category. |
DEFAULT_CATEGORIES | necessary, 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 / rejectAllChoices | The 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.
| Hook | Where | When | Payload |
|---|---|---|---|
cookie-consent:extend | Nuxt (build) | extendCookieConsent adds categories. | (categories: ConsentCategory[]) |
cookie-consent:resolved | Nuxt (build) | Once, after every module has set up. Change the list here. | (categories: ConsentCategory[]) |
cookie-consent:change | Nuxt 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:ready | Nuxt app (client) | The banner is mounted. | (state: ConsentState | null), null while undecided. |
cookie-consent:saved | Nitro | A 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.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/cookie-consent | session only | The signed-in user's stored choice: { consent: ConsentState | null }. |
POST | /api/cookie-consent | session only | Stores { 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.
| Column | Holds |
|---|---|
cookie_consent_accepted_at | The time of the latest choice, in epoch milliseconds. |
cookie_consent_version | The consent version the choice was made against. |
cookie_consent_categories | JSON 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.
| Key | Used for |
|---|---|
cookieConsent.description, accept, reject, preferences, expand, manage, save | The banner and its buttons. |
cookieConsent.preferencesTitle, preferencesDescription, required, cookies | The preferences dialog. |
cookieConsent.policy | The policy link, when policyLabel is not set. |
cookieConsent.categories.<key>.label and .description | Category 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
userstable. - Extension points – how modules add categories without importing each other.