# How to add a cookie consent banner to a Nuxt app

> In Nuxt 3 or 4, declare the snippet in nuxt.config.ts under app.head.script so it is rendered in the head of every page before any other script, or add it with useHead in app.vue. Attributes such as data-site are passed as keys of the script entry, defer: true keeps the defer attribute, and tagPosition: "head" keeps the tag where a consent script belongs.

Source: https://cookiequiet.com/platforms/nuxt
Published: 2026-10-20
Publisher: Cookiequiet

## Steps

1. **Add the script entry to nuxt.config.ts** Under app.head.script, add an object with src set to https://cookiequiet.com/cq.js, defer: true, the data-site, data-consent-mode and data-auto-block keys, and tagPosition set to head. Put it first in the array.
2. **Or use useHead in app.vue** The same object works in a useHead call in app.vue, which applies it to every page. Give it a key so it is never duplicated.
3. **Add your tags as marked entries** Add the Google tag or Meta Pixel as further script entries with type set to text/plain and a data-cq-category key; inline code goes in innerHTML. Being in the server-rendered head, they are activated after consent or on load when a choice is stored.
4. **Or run the Google tag in Consent Mode** Keep the Google tag loading with denied defaults by adding an entry with Google's default consent block in innerHTML between the Cookiequiet entry and the Google tag entry. Modules that add the Google tag for you usually offer a manual initialization or consent option; set the defaults there.
5. **Gate components on consent** For players and widgets rendered by Vue, read window.Cookiequiet.get() in onMounted and subscribe with window.Cookiequiet.on(); render the embed only when the category is granted. For plain iframes, use data-cq-src instead of src.
6. **Add the Cookie preferences link** In the layout footer, render a plain anchor with href="#cq-preferences" and the data-cq-show-preferences attribute.
7. **Test with Reject all** Run the app, open it in a private window, click Reject all and check that no _ga or _fbp cookie is set. Accept all, reload, and check the consent log.

## Where to load the snippet in Nuxt

Nuxt renders the head of every page from `app.head` in `nuxt.config.ts`, through Unhead. A script entry is an object whose keys become attributes, so the snippet translates directly:

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          src: "https://cookiequiet.com/cq.js",
          defer: true,
          "data-site": "YOUR_SITE_KEY",
          "data-consent-mode": "1",
          "data-auto-block": "1",
          tagPosition: "head",
          key: "cookiequiet",
        },
      ],
    },
  },
});
```

Keep it first in the array so it is rendered before any other script of the head. `tagPosition` accepts `head`, `bodyOpen` and `bodyClose`; `head` is the default and the right one here. The same object works in a `useHead` call in `app.vue` if you prefer to keep it next to other head logic; the `key` prevents duplicates. On Nuxt 2, the equivalent is `head.script` in `nuxt.config.js`.

## How to handle the tags your app loads

Cookiequiet activates marked scripts when a stored choice is applied on load and whenever the visitor makes a choice. In a Nuxt app, that means marked tags belong in the server-rendered head, not in components mounted later.

**Marked entries.** Add them to the same `script` array after the Cookiequiet entry; inline code goes in `innerHTML`:

```ts
{ type: "text/plain", "data-cq-category": "analytics", src: "https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX" },
{
  type: "text/plain",
  "data-cq-category": "analytics",
  innerHTML: "window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-XXXXXXX');",
},
```

The Meta Pixel base code gets `"data-cq-category": "marketing"`. Nothing of these runs before the category is granted.

**Consent Mode instead of marking.** To keep Google Analytics or Google Ads loading with denied defaults (cookieless pings, modelled data), insert an entry with Google's default consent block in `innerHTML` between the Cookiequiet entry and the Google tag entry, as shown in the [Google Analytics 4 guide](/platforms/google-analytics-4). Modules that inject the Google tag for you generally offer a manual initialization or consent option; use it to set the defaults to denied, and Cookiequiet sends the update.

**Components that depend on consent.** Read the choice in `onMounted` through the public API: `window.Cookiequiet.get()` returns the stored choice (its `c` array lists the granted categories) and `window.Cookiequiet.on(callback)` calls back after every choice and returns an unsubscribe function. A small composable can expose `granted("marketing")` to a YouTube component, which renders a placeholder with a "Cookie preferences" button until then. For plain iframes in templates, `data-cq-src` instead of `src` is enough.

## How to add the Cookie preferences link

In the layout footer, a plain anchor is all it takes; do not route it through `NuxtLink`:

```html
<a href="#cq-preferences" data-cq-show-preferences>Cookie preferences</a>
```

## How to test

1. Start the app (`nuxi dev` works; the banner fetches its configuration from cookiequiet.com) and open it in a private window.
2. Click **Reject all**: the cookies are `cq_consent` and your own, no `_ga`, no `_fbp`.
3. Clear the cookies, click **Accept all**, reload: the marked entries run, their cookies appear.
4. Navigate between pages: the banner stays closed and the choice is still applied.
5. Check the consent log in the dashboard.

## What is specific to Nuxt

- **SSR, SPA and static.** With `ssr: false`, Unhead still renders the head on the client; with `nuxi generate`, the snippet is in every generated page. All three work.
- **Layers and modules.** A layer or module that adds the Google tag adds it to the same head; check the rendered order in the page source and keep the Cookiequiet entry first.
- **Content Security Policy.** Allow `https://cookiequiet.com` in `script-src` and `connect-src` if you set one.
- **i18n.** The banner's language follows the visitor's browser, with every string editable per language; it does not read the Nuxt locale.

If a tag ends up loaded by a plugin you cannot mark, the auto-blocker still holds the scripts and iframes of about 80 known trackers created with JavaScript until consent, and the [cookie scanner](/tools/cookie-scanner) tells you what is left.

## Questions

### Should I use app.head or useHead?

Both render the same head entry. app.head in nuxt.config.ts is static and applies to every page, which is exactly what a consent script needs; useHead in app.vue is equivalent and lets you read the key from runtime config. Pick one and give the entry a key.

### Does defer: true become a defer attribute?

Yes. Unhead maps boolean keys to boolean attributes and data-* keys to data attributes, so the rendered tag matches the snippet from the dashboard.

### Why does a marked script added by a component not run after consent?

Cookiequiet activates marked scripts when a choice is applied on load and after each new choice. A script injected later waits for the next choice. Put marked tags in the server-rendered head, and gate components through window.Cookiequiet.get() and on() instead.

### Does it work with Nuxt 2?

Yes, through head.script in nuxt.config.js, with the same keys. The rest of the guide (marking, the preferences link, testing) is identical.

## Sources

- [Nuxt: nuxt.config reference (app.head)](https://nuxt.com/docs/4.x/api/nuxt-config)
- [Nuxt: SEO and Meta](https://nuxt.com/docs/4.x/getting-started/seo-meta)
- [Unhead: useHead()](https://unhead.unjs.io/docs/head/api/composables/use-head)
