npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-consent

React 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's runScripts: '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 met type/data-cc weg 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.mjs in 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.tsx zoals 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. Gebruik consent uit useConsent(), of abonneer met onChange(); 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-ring of 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.nl tenzij 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.css

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