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.enabledinsysthema.config.tsdefaults tofalse.SysthemaProvidermounts the banner when you pass an enabled config.- Payload's
RootLayoutreads General Settings and forwards the resolved config. - The supported categories are
necessary,functional,analytics,performanceandadvertisement. Onlynecessaryis 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:
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 configWith 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:
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:
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:
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-cookieconsentimport * 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
- Confirm
cookieConsent.enabledistruein the resolved developer config. - In Payload, enable the editor tab and check its Enable switch if you want editor-managed consent.
- Retain
RootLayoutwithpayloadInstance, or pass the resolver result to the Next.js provider. - Check whether
cc_cookiealready records consent. Clear it for a fresh-session test. - 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