@snippt/cookie-consent
v0.1.3
Published
AVG-conforme cookie-consent voor elk framework
Readme
@snippt/cookie-consent
AVG/GDPR-conforme cookie-consent voor elk framework: vanilla JS, React/Next.js, of een los <script>-tagje op een site zonder buildstap. Geen runtime-dependencies, React is optioneel.
1. Wat het is
@snippt/cookie-consent toont een cookiebanner met gelijkwaardige "Alles accepteren" / "Alles weigeren" / "Voorkeuren"-knoppen, onthoudt de keuze in een cookie, en blokkeert scripts van niet-toegestane categorieën totdat de bezoeker toestemming geeft. Een <script type="text/plain" data-cc="statistics">-tag draait dus pas nadat de bezoeker echt op "Accepteren" (of een passende categorie) heeft geklikt — niet ervoor, en niet stiekem in de achtergrond. De package regelt daarnaast cross-tab synchronisatie, Google Consent Mode v2-signalen, i18n (nl/en), een cookieverklaring-renderer en optionele Snippt-branding.
2. Installatie
npm install @snippt/cookie-consentReact is een optionele peer dependency — je hebt hem alleen nodig als je het @snippt/cookie-consent/react-entrypoint gebruikt. De kernbundel (@snippt/cookie-consent) bevat geen React-code.
Niet-Node sites: los <script>-tagje
Voor een site zonder buildstap (statische HTML, WordPress, een oud CMS) gebruik je de kant-en-klare IIFE-bundel uit dist/:
cp node_modules/@snippt/cookie-consent/dist/cookie-consent.iife.global.js public/vendor/<script src="/vendor/cookie-consent.iife.global.js"></script>
<script>
CookieConsent.init({
policyVersion: '2026-08-13',
privacyPolicyUrl: '/privacy',
});
</script>Deze bundel is geminificeerd, bevat geen React en injecteert haar eigen stylesheet automatisch (zie styles in de configuratietabel) — je hoeft dus niets extra's te laden om er een werkende banner mee neer te zetten.
3. Snelstart React
// app/layout.tsx
import '@snippt/cookie-consent/styles.css';
import { ConsentProvider, ConsentModeScript } from '@snippt/cookie-consent/react';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="nl">
<head>
{/* Vóór je GTM/gtag-tag: zet de "alles denied"-default direct,
zodat tags niet even ongeconsenteerd kunnen afvuren terwijl
React nog moet hydrateren. */}
<ConsentModeScript />
</head>
<body>
<ConsentProvider
config={{
policyVersion: '2026-08-13',
privacyPolicyUrl: '/privacy',
googleConsentMode: true,
// We importeren de stylesheet hierboven al via '@snippt/cookie-consent/styles.css';
// zonder dit zou init() dezelfde CSS nóg een keer als inline <style> injecteren.
styles: false,
}}
>
{children}
</ConsentProvider>
</body>
</html>
);
}Ergens dieper in de boom:
'use client';
import { useConsent, ConsentGate } from '@snippt/cookie-consent/react';
function CookieSettingsLink() {
const { openPreferences } = useConsent();
return <button onClick={openPreferences}>Cookievoorkeuren aanpassen</button>;
}
function Marketing() {
return (
<ConsentGate category="marketing" fallback={<p>Deze content vereist marketingcookies.</p>}>
<FacebookPixel />
</ConsentGate>
);
}ready, en waarom die er is
useConsent() geeft { ready, consent, hasConsent, accept, denyAll, withdraw, openPreferences } terug. ready wordt pas true na dat ConsentProvider zijn init() heeft gedraaid.
React draait de effects van kind-componenten vóór die van hun ouder. Een component die zelf in een useEffect bij mount accept(), denyAll(), withdraw() of openPreferences() aanroept, doet dat dus potentieel vóórdat ConsentProvider de kern heeft geïnitialiseerd — en die functies gooien dan een fout in plaats van iets te doen:
// FOUT: draait mogelijk vóór init(), gooit
function Broken() {
const { openPreferences } = useConsent();
useEffect(() => { openPreferences(); }, []); // kan gooien
return null;
}
// GOED: wacht op ready
function Fixed() {
const { ready, openPreferences } = useConsent();
useEffect(() => {
if (!ready) return;
openPreferences();
}, [ready, openPreferences]);
return null;
}Dit kost een middag debuggen als je het niet weet — schrijf het dus in elke component die bij mount een van deze vier functies aanroept.
4. Snelstart vanilla
<link rel="stylesheet" href="node_modules/@snippt/cookie-consent/dist/styles.css">
<script type="module">
import { init } from '@snippt/cookie-consent';
init({
policyVersion: '2026-08-13',
privacyPolicyUrl: '/privacy',
// De <link> hierboven laadt de stylesheet al; zonder dit injecteert
// init() dezelfde CSS nóg een keer als inline <style>.
styles: false,
});
</script>init() toont direct de banner (als er nog geen geldige keuze staat) of, na een eerdere keuze, alleen het zwevende knopje om voorkeuren aan te passen. Je hoeft niets te renderen — de library manipuleert zelf het DOM.
5. Scripts blokkeren
Zet elk script dat pas na toestemming mag draaien op type="text/plain" met een data-cc-attribuut dat de categorie aangeeft (preferences, statistics of marketing):
<script type="text/plain" data-cc="statistics"
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script type="text/plain" data-cc="statistics">
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>Zodra de bezoeker een categorie toestaat (bij init() met een bestaand record, of via accept()), vervangt de library elk bijpassend <script type="text/plain" data-cc="...">-element door een vers, uitvoerbaar <script>-element: alle overige attributen (zoals src) worden overgenomen, type en data-cc worden weggelaten, en inline-inhoud blijft behouden. Dat vervangen (in plaats van alleen het type-attribuut aanpassen) is nodig omdat browsers een script pas uitvoeren als het als nieuw element in het document wordt gezet.
Een eventuele nonce gaat mee naar het vervangende element, zodat de scripts ook onder een nonce-gebaseerd CSP draaien.
Komt er ná init() nieuwe markup in de pagina (een AJAX-blok, een client-side routewissel, een widget die zichzelf injecteert), roep dan rescan() aan: die scant opnieuw en activeert wat volgens de huidige keuze mag.
import { rescan } from '@snippt/cookie-consent';
router.afterEach(() => rescan());Bekende testbeperking: de geautomatiseerde suite (jsdom via Vitest) kan dynamisch ingevoegde
<script>-elementen niet echt uitvoeren — ook niet met jsdom'srunScripts: 'dangerously', omdat Vitest tests in een aparte VM-context draait. De tests verifiëren daarom het DOM-mechanisme (het geblokkeerde element wordt vervangen door een uitvoerbaar element mettype/data-ccweg en de inhoud intact), niet dat het script daadwerkelijk draait. Neem een handmatige controle in een echte browser op in je opleverchecklist per site —demo.html+scripts/browser-check.mjsin deze repo doen precies dat, inclusief het bevestigen dat een geblokkeerd script echt uitvoert in Chromium.
Google Analytics 4 (kant-en-klaar)
Voor GA4 hoef je de twee scripts hierboven niet met de hand te tikken. googleAnalyticsSnippet(gaId) genereert exact diezelfde gegate markup, en gooit een duidelijke fout als gaId niet met G- begint:
import { googleAnalyticsSnippet } from '@snippt/cookie-consent';
// Vervang G-XXXXXXXXXX per klant door hun eigen GA4 measurement-ID —
// die is nooit hetzelfde tussen twee sites.
document.body.insertAdjacentHTML('beforeend', googleAnalyticsSnippet('G-XXXXXXXXXX'));In React is er <GoogleAnalytics gaId="..." />:
import { GoogleAnalytics } from '@snippt/cookie-consent/react';
<GoogleAnalytics gaId="G-XXXXXXXXXX" /> {/* per klant: hun eigen measurement-ID */}Beide zetten de scripts vast op categorie statistics. Denk nog wel zelf aan cookiesToClear: { statistics: ['_ga', '_ga_<jouw-id-zonder-G->'] } in je config, zodat withdraw() en het intrekken via het voorkeurenpaneel de GA-cookies ook echt opruimen — de helper kent je measurement-ID niet vooraf en kan dat niet voor je raden.
6. Configuratie
init({
policyVersion: string, // verplicht
privacyPolicyUrl: string, // verplicht
// alles hieronder is optioneel, met de genoemde default
});| Veld | Type | Default | Betekenis |
|---|---|---|---|
| policyVersion | string | — (verplicht) | Versie van je cookiebeleid. Wijzig deze waarde om elke bezoeker opnieuw te laten vragen (zie §10). |
| privacyPolicyUrl | string | — (verplicht) | URL naar je privacybeleid; wordt gelinkt vanuit de banner. |
| cookieName | string | 'cc_consent' | Naam van het cookie waarin de keuze wordt opgeslagen. |
| cookieMaxAgeDays | number | 182 | Levensduur van het consent-cookie in dagen. Moet groter dan 0 zijn. |
| cookieDomain | string | (host-only) | domain=-attribuut van het cookie; laat leeg voor een host-only cookie. |
| locale | 'auto' \| 'nl' \| 'en' | 'auto' | 'auto' detecteert document.documentElement.lang / navigator.language, met terugval op defaultLocale. |
| defaultLocale | 'nl' \| 'en' | 'nl' | Taal als detectie niets bruikbaars oplevert. |
| translations | Record<string, unknown> | {} | Per-locale overrides, diep gemerged over de ingebouwde nl/en-teksten. |
| position | 'bottom' \| 'bottom-left' \| 'center' | 'bottom' | Plaatsing van het bannerpaneel. |
| styles | boolean | true | Injecteert automatisch een <style data-cc-styles>-tag met de ingebouwde CSS. Zet op false als je zelf @snippt/cookie-consent/styles.css importeert of een eigen stylesheet levert. |
| icon | boolean \| string | true | true toont het ingebouwde koekje-icoon, false toont geen icoon, een string wordt als eigen (SVG-)HTML gebruikt. |
| googleConsentMode | boolean | false | Stuurt Google Consent Mode v2-signalen (consent/default en consent/update) naar window.dataLayer. Zie de toelichting hieronder. |
| reloadOnWithdraw | boolean | true | Herlaadt de pagina na withdraw(), zodat al geladen third-party scripts (die niet met JavaScript ongedaan te maken zijn) écht verdwijnen. |
| debug | boolean | false | Logt bij init() de opgeloste config en huidige keuze naar de console. |
| branding | boolean | false | Toont een kleine "Cookie-instellingen door"-link met Snippt-woordmerk in de banner en het voorkeurenpaneel, en het volledige logo in de cookieverklaring. Zie §11. |
| brandingUrl | string | 'https://snippt.nl' | Doel van de brandinglink. |
| brandColor | string | (Snippt-groen) | Overschrijft de kleur van het woordmerk op alle drie plekken. Elke geldige CSS-kleurwaarde. Zie §11. |
| backgroundColor | string | (wit / donkergrijs) | Achtergrondkleur van het bannerpaneel. Zie §8. |
| textColor | string | (zwart / wit) | Kleur van titel én beschrijvingstekst samen. Zie §8. |
| acceptColor | string | (Snippt-groen) | Achtergrondkleur van alleen de accepteerknop — onafhankelijk van accentColor. Zie §8. |
| denyColor | string | (lichtgrijs) | Achtergrondkleur van alleen de weigerknop. Zie §8. |
| accentColor | string | (Snippt-groen) | Kleur van links, schakelaars en de focusrand — niet de knoppen. Zie §8. |
| cookiesToClear | Partial<Record<Category, string[]>> | {} | Cookienamen die withdraw() per categorie expliciet moet verwijderen (naast het consent-cookie zelf), bv. { statistics: ['_ga', '_gid'] }. |
| declaration | Partial<Record<Category, CookieInfo[]>> | {} | Voedt de cookieverklaring-tabel. Zie §9. |
Category is 'necessary' | 'preferences' | 'statistics' | 'marketing'. necessary staat altijd aan en kan niet uitgezet worden.
Google Consent Mode
Met googleConsentMode: true stuurt init() bij het laden een consent/default-signaal (alles denied behalve security_storage, wait_for_update: 500) en, zodra er een bestaand of nieuw record is, een consent/update-signaal met de werkelijke keuze per categorie (marketing → ad_*, statistics → analytics_storage, preferences → functionality_storage/personalization_storage).
Zet daarnaast CONSENT_MODE_SNIPPET (of <ConsentModeScript /> in React) vóór je GTM-containertag in de <head>, zodat de default al staat voordat Tag Manager zelf laadt:
<script>
window.dataLayer=window.dataLayer||[];
function gtag(){dataLayer.push(arguments);}
gtag('consent','default',{"ad_storage":"denied","ad_user_data":"denied","ad_personalization":"denied","analytics_storage":"denied","functionality_storage":"denied","personalization_storage":"denied","security_storage":"granted","wait_for_update":500});
</script>
<script src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX"></script>Deze twee mogen naast elkaar bestaan: init() scant window.dataLayer op een reeds aanwezige consent default (van het snippet hierboven, of van je eigen code) en slaat de tweede push dan over, zodat de wait_for_update-timer niet opnieuw wordt gearmd.
Bekende beperking (React):
<ConsentModeScript />is getest en gedocumenteerd voor de App Router. In de Pages Router is de volgorde ten opzichte van je GTM-tag niet gegarandeerd en dedupliceert de component zichzelf niet; gebruik daar het inline<script>uit_document.tsxzoals hierboven. React 18 is ongetest.
Verifieer dit één keer met GTM Preview / Tag Assistant voordat een advertentieklant live gaat in de EER. De signalen worden correct verstuurd (dat is getest), maar of jouw specifieke GTM-container en tags er daadwerkelijk naar luisteren, hangt af van hoe die container is ingericht. Controleer per site dat advertentietags pas afvuren ná een
granted-signaal.
7. API
Alles hieronder komt uit het hoofdentrypoint (import { ... } from '@snippt/cookie-consent'), tenzij anders vermeld.
| Functie | Uitleg |
|---|---|
| init(config: CookieConsentConfig): void | Initialiseert de banner: leest een bestaand consent-cookie, injecteert stijl (indien styles), activeert eerder toegestane scripts, en toont de banner of het zwevende knopje. |
| getConsent(): ConsentRecord \| null | Het huidige opgeslagen record, of null zolang er geen (geldige) keuze is. |
| hasConsent(category: Category): boolean | true voor 'necessary'; voor overige categorieën het huidige record, of false zonder record. |
| accept(categories: Category[] \| 'all'): void | Slaat een nieuwe keuze op. Vervangt de volledige set, merget nooit met de vorige keuze: na accept(['statistics','marketing']) gevolgd door accept(['statistics']) staat marketing weer op false. mode wordt 'all' bij accept('all'); bij een expliciete lijst is mode altijd 'custom' — ook als die lijst toevallig alle categorieën bevat. |
| denyAll(): void | Zet alle optionele categorieën uit; slaat op met mode: 'none'. |
| withdraw(): void | Wist het consent-cookie én alle cookies uit cookiesToClear, zet Consent Mode (indien aan) op denied, stuurt een wijzigingsevent met null, en herlaadt de pagina als reloadOnWithdraw (default true) aanstaat. |
| openPreferences(): void | Toont het voorkeurenpaneel. |
| onChange(cb: (record: ConsentRecord \| null) => void): () => void | Abonneert op elke wijziging — ook vanuit andere tabs, via BroadcastChannel. Geeft een unsubscribe-functie terug. |
| rescan(): number | Scant opnieuw op geblokkeerde scripts en activeert wat volgens de huidige keuze mag. Nodig voor markup die ná init() in de pagina komt (AJAX-blok, client-side routewissel, CMS-widget). Geeft het aantal geactiveerde scripts terug. |
| destroy(): void | Ruimt de eigen event-listeners, gemonteerde UI en interne state op. Je eigen onChange-abonnees blijven staan — die meld je zelf af met de teruggegeven functie. Vooral relevant bij React-unmount of hot-reload in development. |
| getResolvedConfig(): ResolvedConfig | De config nadat alle defaults zijn toegepast. Gooit een fout als init() nog niet is aangeroepen. |
| getUiContext(): UiContext | De context die de ingebouwde UI zelf gebruikt. Alleen nodig als je een eigen renderer bouwt; renderDeclaration(target) haalt hem zelf op. |
| getTranslations(config: ResolvedConfig): Translations | De samengevoegde teksten voor de opgeloste taal, inclusief je eigen translations-overrides. |
| renderDeclaration(target: HTMLElement, ctx?: UiContext): void | Rendert de cookieverklaring in target. Laat ctx weg: die wordt uit de kern opgebouwd (zie §9). De tweeargumentsvorm is voor eigen wrappers met een eigen context, zoals <CookieDeclaration />. |
| googleAnalyticsSnippet(gaId: string): string | Genereert de twee gegate GA4-scripts (categorie statistics) als HTML-string. Gooit als gaId niet met G- begint. Zie §5. |
| CONSENT_MODE_SNIPPET: string | Kant-en-klare inline <script>-inhoud die de Google Consent Mode-default zet. Zie §6. |
| EVENT_NAME: 'cc:change' | Naam van het CustomEvent dat op window wordt gevuurd bij elke wijziging (event.detail is het nieuwe ConsentRecord \| null). |
| VERSION: string | Packageversie. |
Daarnaast exporteert de package de types Category, ConsentRecord, ConsentCategories, ConsentMode, CookieConsentConfig, CookieInfo, Locale, ResolvedConfig, UiContext en Translations.
getConsent() geeft een kopie terug: het record is het juridische bewijs van de keuze en mag niet van buitenaf te wijzigen zijn.
Let op bij React: omdat het een kopie is, verschilt de referentie bij elke aanroep. Zet
getConsent()daarom nooit in een dependency array — dat levert een oneindige renderlus op. GebruikconsentuituseConsent(), of abonneer metonChange(); beide leveren een stabiele referentie die alleen verandert wanneer de keuze zelf verandert.
locale, defaultLocale en position worden gevalideerd; een onbekende waarde levert direct een [cookie-consent] ...-fout op in plaats van een halfgerenderde banner.
@snippt/cookie-consent/react
| Export | Uitleg |
|---|---|
| <ConsentProvider config={...}> | Roept init() aan bij mount en destroy() bij unmount, en biedt de context waar useConsent() uit leest. |
| useConsent(): ConsentApi | { ready, consent, hasConsent, accept, denyAll, withdraw, openPreferences }. Zie §3 voor de ready-valkuil. |
| <ConsentGate category="statistics" fallback={...}> | Toont children alleen als hasConsent(category) waar is, anders fallback (default null). |
| <ConsentModeScript /> | Rendert CONSENT_MODE_SNIPPET inline; hoort in de <head>, vóór je GTM-tag. |
| <CookieDeclaration /> | React-wrapper rond renderDeclaration; rendert automatisch opnieuw als de consent-status wijzigt. Zie §9. |
| <GoogleAnalytics gaId="G-..." /> | React-wrapper rond googleAnalyticsSnippet. Zie §5. |
ConsentApi is ook als type te importeren.
8. Theming
Per klant via config (geen CSS nodig)
Voor de zes dingen die je per klant realistisch wilt aanpassen is er een losse config-optie — geen stylesheet nodig, geen kennis van CSS-variabelen:
init({
policyVersion: '2026-08-13',
privacyPolicyUrl: '/privacy',
backgroundColor: '#fff7e6', // achtergrond van het paneel
textColor: '#3a2d1a', // titel + beschrijving samen
acceptColor: '#0044cc', // alleen de accepteerknop
denyColor: '#cc0000', // alleen de weigerknop
accentColor: '#9900cc', // links, schakelaars, focusrand — niet de knoppen
brandColor: '#02563c', // het Snippt-woordmerk, zie §11
});Elk veld werkt onafhankelijk van de andere: acceptColor verandert nooit de kleur van de links, en accentColor verandert nooit de knop. De tekst op beide knoppen blijft wit — kies voor acceptColor/denyColor geen erg lichte tint, anders wordt die combinatie moeilijk leesbaar. Weiger- en accepteerknop blijven qua grootte en vulling exact gelijk; alleen de kleur zelf verschilt, zodat de keuze niet gestuurd wordt.
Cursor blijft zichtbaar op custom-cursor-sites
Elk klikbaar element in de banner (cc-btn, cc-switch, cc-fab, cc-link) zet cursor: pointer !important. Dat !important is bewust: sommige sites verbergen de systeemcursor overal met * { cursor: none !important; } voor een eigen custom cursor-effect (een lavendel stip die met de muis meebeweegt, bijvoorbeeld). Zonder !important verliest een gewone cursor: pointer die strijd altijd — ongeacht selector-specificiteit — en is geen enkel element in de banner nog als klikbaar herkenbaar voor de bezoeker.
Als je site zelf zo'n custom cursor heeft: controleer of het z-index van jouw cursor-element (
#cursor-dot/#cursor-ringof vergelijkbaar) hoger staat dan--cc-z(2147483000). Staat het lager, dan komt onze banner er wél overheen — de systeemcursor is dan zichtbaar boven de banner (dankzij de fix hierboven), maar jouw eigen cursor-effect niet.
Handmatig via CSS-variabelen
Voor alles wat de zes snelkoppelingen hierboven niet dekken (randen, afronding, schaduw, lettertype): alle stijlen zitten achter CSS-variabelen op .cc-root, met een prefers-color-scheme: dark-override. Overschrijf ze in je eigen stylesheet (na het importeren van @snippt/cookie-consent/styles.css, of vóór/i.p.v. styles: true):
De library zet haar eigen tokens in :where(.cc-root) — specificiteit nul — zodat jouw .cc-root-regel altijd wint, ook als de library haar stylesheet zelf injecteert (styles: true) en die dus ná de jouwe in de <head> staat.
.cc-root {
--cc-bg: #ffffff; /* achtergrond van paneel en FAB */
--cc-fg: #1a1a1a; /* titel */
--cc-muted: #5c5c5c; /* beschrijvingen, bijschriften */
--cc-border: #e0e0e0; /* randen, scheidingslijnen */
--cc-accept-bg: #02563c; /* vulling van de accepteerknop */
--cc-accept-fg: #ffffff; /* tekstkleur op de accepteerknop */
--cc-neutral: #e6e8ea; /* vulling van de weigerknop */
--cc-neutral-fg: #1a1a1a; /* tekstkleur op de weigerknop */
--cc-accent: #02563c; /* links, focus-ring, aan-stand van de toggle — niet de knoppen */
--cc-radius: 14px; /* afronding van het paneel */
--cc-font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--cc-shadow: 0 -6px 32px rgba(0, 0, 0, 0.14);
--cc-z: 2147483000; /* z-index van banner, FAB en voorkeurenpaneel */
--cc-cookie-base: #d9a05b; /* koekje-icoon: deeg */
--cc-cookie-chip: #5b3a1e; /* koekje-icoon: chocoladestukjes */
--cc-brand: #02563c; /* kleur van het Snippt-woordmerk */
}
@media (prefers-color-scheme: dark) {
.cc-root {
--cc-bg: #16181a;
--cc-fg: #f2f2f2;
--cc-muted: #a8a8a8;
--cc-border: #2e3134;
--cc-accept-bg: #0a9367;
--cc-accent: #0a9367;
--cc-neutral: #333739;
--cc-neutral-fg: #f2f2f2;
--cc-brand: #0a9367;
--cc-shadow: 0 -6px 32px rgba(0, 0, 0, 0.5);
}
}De config-velden uit de vorige paragraaf zetten precies deze variabelen inline op het root-element (backgroundColor → --cc-bg, textColor → --cc-fg én --cc-muted, acceptColor → --cc-accept-bg, denyColor → --cc-neutral, accentColor → --cc-accent) — beide routes zijn dus verschillende ingangen tot dezelfde tokens.
position: 'bottom' | 'bottom-left' | 'center' (zie §6) bepaalt de plaatsing van het paneel; kleuren, radius en font pas je hierboven aan. De "Accepteren"- en "Weigeren"-knoppen delen dezelfde maatvoering (.cc-btn--choice) en declareren exact dezelfde eigenschappen — beide zijn gevuld, alleen de tint verschilt. Dat is bewust: een omlijnde weigerknop naast een massieve accepteerknop leest als een aanbeveling, en een toezichthouder rekent daarop af. Houd --cc-neutral daarom net zo leesbaar als --cc-accent als je ze aanpast.
9. Cookieverklaring
Vul declaration in de config met de cookies die je site daadwerkelijk zet, per categorie:
init({
policyVersion: '2026-08-13',
privacyPolicyUrl: '/privacy',
declaration: {
statistics: [
{ name: '_ga', provider: 'Google Analytics', purpose: 'Onderscheidt unieke bezoekers.', expiry: '2 jaar', type: 'HTTP' },
{ name: '_ga_XXXXXXX', provider: 'Google Analytics', purpose: 'Houdt sessiestatus bij.', expiry: '2 jaar', type: 'HTTP' },
],
marketing: [
{ name: '_fbp', provider: 'Meta', purpose: 'Advertentie-targeting.', expiry: '3 maanden', type: 'HTTP' },
],
},
});CookieInfo velden: name, provider, purpose, expiry (vrije tekst, bv. '2 jaar'), type ('HTTP' | 'HTML' | 'Pixel'). Categorieën zonder entries tonen "Geen cookies in deze categorie."
Render de verklaring op je cookiepagina met de React-component:
import { CookieDeclaration } from '@snippt/cookie-consent/react';
export default function CookiesPage() {
return <CookieDeclaration />;
}<CookieDeclaration /> moet binnen <ConsentProvider> staan. Ze toont per categorie een "Toegestaan"/"Geweigerd"-badge op basis van het huidige record, en een knop om de toestemming in te trekken.
Bouw je vanilla (geen React, of een IIFE-embed op WordPress/Webflow), roep dan renderDeclaration aan met alléén het doelelement — de context wordt uit de kern opgebouwd:
<div id="cookieverklaring"></div>
<script>
CookieConsent.init({ policyVersion: '2026-08-13', privacyPolicyUrl: '/privacy', declaration: { /* ... */ } });
CookieConsent.renderDeclaration(document.getElementById('cookieverklaring'));
</script>Zet deze verklaring op elke site neer. Ze bevat de enige intrekknop in de geleverde UI: het voorkeurenpaneel heeft alleen "Terug" en "Opslaan", dus zonder cookieverklaring heeft de bezoeker geen manier om zijn toestemming volledig in te trekken. (Een categorie uitzetten in het voorkeurenpaneel kan wel: dat wist ook de cookiesToClear-cookies van die categorie en herlaadt de pagina als reloadOnWithdraw aanstaat.)
Roep renderDeclaration opnieuw aan na een wijziging als je de status live wilt bijwerken:
onChange(() => renderDeclaration(document.getElementById('cookieverklaring')!));Heb je een eigen renderer met eigen acties, gebruik dan de tweeargumentsvorm met een UiContext (ook als type geëxporteerd):
interface UiContext {
config: ResolvedConfig; // getResolvedConfig()
t: Translations; // getTranslations(getResolvedConfig())
getRecord(): ConsentRecord | null; // getConsent
acceptAll(): void; // () => accept('all')
denyAll(): void; // denyAll
saveCustom(selection: Record<Exclude<Category, 'necessary'>, boolean>): void;
withdraw(): void; // withdraw
}saveCustom wordt door renderDeclaration zelf niet aangeroepen (de verklaring toont alleen status en een intrekknop, geen per-categorie toggles) — een no-op volstaat als je geen voorkeurenpaneel zelf bouwt. getUiContext() geeft precies dit object voor de huidige kern-state.
10. Toestemming vernieuwen
Wijzig je het cookiebeleid inhoudelijk — een nieuwe leverancier, een andere bewaartermijn, een nieuwe advertentiepartner — verhoog dan policyVersion (bv. van '2026-08-13' naar '2026-09-01', of een simpel oplopend nummer):
init({
policyVersion: '2026-09-01', // was '2026-08-13'
privacyPolicyUrl: '/privacy',
});Elk opgeslagen consent-record bevat de policyVersion waarmee het is aangemaakt (pv). Bij het lezen van het cookie wordt een record met een andere pv genegeerd, alsof er nooit een keuze is gemaakt — de banner verschijnt dan automatisch weer voor iedereen, ongeacht hoe lang cookieMaxAgeDays nog loopt. Je hoeft dus nooit zelf cookies te legen; het ophogen van dit ene veld is de volledige procedure.
11. Branding
Zet branding: true om aan je klant kenbaar te maken dat Snippt de cookie-oplossing heeft geleverd:
init({
policyVersion: '2026-08-13',
privacyPolicyUrl: '/privacy',
branding: true,
// brandingUrl: 'https://snippt.nl', // default, override indien gewenst
// brandColor: '#d0006f', // optioneel: eigen kleur voor het woordmerk
});Dit voegt drie dingen toe, telkens gecentreerd onderaan, ná de knoppen:
- Een kleine tekstlink "Cookie-instellingen door" met het Snippt-woordmerk ernaast (geen los "Snippt" in de tekst — het logo toont de merknaam al), onderin zowel de banner als het voorkeurenpaneel — beide linken naar
brandingUrl(https://snippt.nltenzij overschreven). - Het volledige Snippt-logo bovenaan de cookieverklaring (
<CookieDeclaration />/renderDeclaration).
brandColor overschrijft de kleur van het woordmerk op alle drie plekken tegelijk — handig als de standaard Snippt-groene tint niet bij de huisstijl van de klant past. Elke geldige CSS-kleurwaarde werkt ('#d0006f', 'rgb(208 0 111)', een named color). Laat weg voor de standaard tint.
Default is false — branding verschijnt nergens tenzij je hem expliciet aanzet. Zet dit alleen aan met toestemming van de klant.
12. Migreren vanaf de oude template
Er is geen automatische migratie vanaf de losse CookieBanner.tsx / cookie-consent.ts / GoogleAnalytics.tsx-bestanden die dit repository voorheen bevatte, en die is ook niet nodig. De oude template sloeg een kale string ('necessary' | 'all' | null) op in een cookie genaamd cookie_consent; deze package gebruikt een ander cookienaam (cc_consent by default) en een ander, gestructureerd recordformaat ({ v, pv, ts, id, mode, categories }). Omdat de cookienamen niet overlappen, wordt het oude cookie simpelweg genegeerd — er valt niets te parsen of om te zetten.
Praktisch betekent dit: zodra je een site van de oude template naar @snippt/cookie-consent overzet, ziet de bezoeker eenmalig opnieuw de banner en maakt een verse keuze. Dat is de bedoeling, geen bug — het is dezelfde uitkomst als een policyVersion-bump (§10), alleen dan automatisch doordat het cookie al niet meer bestaat volgens de nieuwe naam. Verwijder per site de drie oude bestanden (cookie-consent.ts, CookieBanner.tsx, GoogleAnalytics.tsx) zodra je hem hebt overgezet.
Development
npm install
npm run typecheck # tsc --noEmit
npm test # vitest run
npm run build # prebuild (genereert src/ui/brand.ts en src/ui/styles.ts) + tsup + kopieert dist/styles.csssrc/ui/brand.ts en src/ui/styles.ts zijn gegenereerd bestanden (uit snippt.svg resp. src/ui/styles.css) en staan mee in git; bewerk ze niet met de hand — pas de bron aan en draai npm run build (of los node scripts/build-brand.mjs / node scripts/build-styles.mjs).
Publiceren
npm publish draait automatisch prepublishOnly (npm run typecheck && npm test && npm run build) — publiceren kan dus niet per ongeluk een verouderde of afwezige dist/ uitleveren, ook niet als je vergeet zelf eerst te bouwen.
