Docs

This page isn't translated yet

next

Cookie consent

Set up the consent banner in Payload or Next.js, reopen preferences and gate optional scripts.

On this page

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 glanceLink to this section

  • 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 discovers source-level cookie usage. Check runtime behavior separately.
  • Theming explains token bindings and overrides.

Quick startLink to this section

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

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 for overrides.

Project setupLink to this section

Payload and Next.jsLink to this section

Enable the editor tab in your existing SysthemaPayloadPluginOptions object:

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:

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.

Next.js without PayloadLink to this section

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

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 HTMLLink to this section

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 for output paths. Loading those files is not a workaround for the current failure.

Reopen patternLink to this section

Add a persistent link to your site footer:

<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:

pnpm add vanilla-cookieconsent
import * as CookieConsent from 'vanilla-cookieconsent'

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

Gating third-party scriptsLink to this section

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

<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.

TroubleshootingLink to this section

The banner does not appearLink to this section

  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 automationLink to this section

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 languageLink to this section

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 for fallback behavior.

Scanning or seeding failsLink to this section

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

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