---
title: "Cookie consent"
description: "Set up the consent banner in Payload or Next.js, reopen preferences and gate optional scripts."
requested_language: cs
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/cs/guides/cookie-consent
version: unreleased (main)
docs_index: https://docs.systhema.app/cs/llms.txt
---
> This page isn't translated yet. Showing English.


Use Systhema's integration with vanilla-cookieconsent v3 to display a consent banner and hold optional scripts until the visitor accepts their category. Enable it explicitly and verify your site's tracking behavior.

## At a glance

- `cookieConsent.enabled` in `systhema.config.ts` defaults to `false`.
- `SysthemaProvider` mounts the banner when you pass an enabled config.
- Payload's `RootLayout` reads General Settings and forwards the resolved config.
- The supported categories are `necessary`, `functional`, `analytics`, `performance` and `advertisement`. Only `necessary` is on and read-only by default.
- [Cookie scanner](scanner.md) discovers source-level cookie usage. Check runtime behavior separately.
- [Theming](theming.md) explains token bindings and overrides.

## Quick start

Merge `cookieConsent` into your existing config, retaining its design inputs and package choices:

```ts title="systhema.config.ts"
import type { SysthemaConfig } from '@systhemaui/core'
import manifest from './src/tokens/manifest.json'

const config: SysthemaConfig = {
  manifest,
  packages: { react: true },
  cookieConsent: {
    enabled: true,
    policyLinks: [{ label: 'Privacy policy', url: '/privacy-policy' }],
  },
}

export default config
```

With default tokens and configuration, the banner uses English copy, opt-in categories, bottom-right placement and a `cc_cookie` consent cookie with a 365-day lifetime. Review the [Configuration shape](configuration.md#configuration-shape) for overrides.

## Project setup

### Payload and Next.js

Enable the editor tab in your existing `SysthemaPayloadPluginOptions` object:

```ts title="Plugin options to merge"
import type { SysthemaPayloadPluginOptions } from '@systhemaui/payload'

export const consentOptions: SysthemaPayloadPluginOptions = {
  generalSettings: { cookieConsent: { enabled: true } },
}
```

The tab is off by default. Enabling it registers the editing controls; it does not replace the master switch in `systhema.config.ts`. The editor's Enable switch can turn off an enabled banner.

Retain the starter's Header and Footer and pass a Payload promise to its `RootLayout`:

```tsx title="src/app/(site)/template.tsx"
import type { ReactNode } from 'react'
import configPromise from '@payload-config'
import { getPayload } from 'payload'
import { RootLayout, RootHeader, RootFooter } from '@systhemaui/payload/next'

export default function SiteTemplate({ children }: { children: ReactNode }) {
  const payloadInstance = getPayload({ config: configPromise })

  return (
    <RootLayout payloadInstance={payloadInstance}>
      <RootHeader payloadInstance={payloadInstance} />
      {children}
      <RootFooter payloadInstance={payloadInstance} />
    </RootLayout>
  )
}
```

This example shows the consent wiring. Preserve your existing fonts, logo, CSS import and responsive `elementProps`. For localized sites, pass the shell's route locale rather than overriding `<html lang>` with English.

`RootLayout` seeds the consent fields on first initialization, resolves editor overrides on the server and passes them to `SysthemaProvider`. Cleared editor fields stay cleared. If General Settings or its consent data is unavailable, resolution can fall back to developer configuration. See [Editing consent in Payload](editor.md).

### Next.js without Payload

The Next.js starter already passes the resolver result to its provider. For an existing app:

```tsx title="src/app/layout.tsx"
import type { ReactNode } from 'react'
import { SysthemaProvider, getSysthemaCookieConsentConfig } from '@systhemaui/next'
import './globals.css'

export default function RootLayout({ children }: { children: ReactNode }) {
  const cookieConsent = getSysthemaCookieConsentConfig()

  return (
    <html lang="en">
      <body data-theme="default" className="bg-layout-main">
        <a className="skip-link" href="#main-content">
          Skip to content
        </a>
        <SysthemaProvider cookieConsent={cookieConsent}>{children}</SysthemaProvider>
      </body>
    </html>
  )
}
```

The resolver returns `null` when disabled, so the provider mounts no banner. This does not imply that the package has no other runtime cost. Do not mount a second `CookieConsentBanner` alongside the provider's instance; vanilla-cookieconsent is a singleton.

### Plain HTML

> [!WARNING]
> The shipped vanilla JavaScript bundle currently throws on load in browsers. Its HTML consent initialization is blocked by that product bug. Verify a fixed release before relying on this integration.

The intended HTML integration reads `window.__SYSTHEMA_COOKIE_CONSENT__` before loading the IIFE. The IIFE is `dist/js/bundle.global.js`; `dist/js/bundle.js` is ESM. A window config must include its own translations because it does not pass through core's merged server configuration.

Use [HTML template](https://docs.systhema.app/cs/getting-started/templates/html.md#the-bundled-javascript) for output paths. Loading those files is not a workaround for the current failure.

## Reopen pattern

Add a persistent link to your site footer:

```html
<a href="#cookie-settings">Cookie settings</a>
```

Override `cookieConsent.reopenSelector` to change the selector, or pass `false` to disable the automatic click listener. For programmatic control in a browser module, add `vanilla-cookieconsent` as a direct dependency:

```bash
pnpm add vanilla-cookieconsent
```

```ts
import * as CookieConsent from 'vanilla-cookieconsent'

export function openCookiePreferences() {
  CookieConsent.showPreferences()
}
```

## Gating third-party scripts

The integration sets `manageScriptTags: true`. Give an optional script `type="text/plain"` and its supported `data-category`; vanilla-cookieconsent activates it after acceptance.

```html
<script type="text/plain" data-category="analytics">
  window.dispatchEvent(new CustomEvent('analytics-consent-granted'))
</script>
```

Replace this illustrative event with your reviewed tracking initialization. Category gating does not implement Google Consent Mode v2 or undo network requests already made by an ungated script. Test acceptance, rejection and withdrawal with the actual integration.

## Troubleshooting

### The banner does not appear

1. Confirm `cookieConsent.enabled` is `true` in the resolved developer config.
2. In Payload, enable the editor tab and check its Enable switch if you want editor-managed consent.
3. Retain `RootLayout` with `payloadInstance`, or pass the resolver result to the Next.js provider.
4. Check whether `cc_cookie` already records consent. Clear it for a fresh-session test.
5. Read the browser console for initialization errors.

### The banner is hidden under automation

`hideFromBots` defaults to `true`. Set `cookieConsent.hideFromBots: false` for an automated check. The runtime logs a suppression reason when `navigator.webdriver` or the user agent triggers the bot check.

### The banner uses the wrong language

Check the route's `<html lang>` and its matching translation block. In a localized Payload shell, remove a fixed `htmlAttributes={{ lang: 'en' }}` override and pass the route locale. See [Translations](configuration.md#translations) for fallback behavior.

### Scanning or seeding fails

Check the configured source globs and read `.systhema/cookies.discovered.json`. For the Payload starter, seed with its explicit config path:

```bash
systhema cookies seed --payload-config src/payload.config.ts
```

## Related

- [Consent configuration](configuration.md)
- [Editing consent in Payload](editor.md)
- [Cookie scanner](scanner.md)
- [Theming the banner](theming.md)
