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

@meui-creative/cookies

v7.0.3

Published

GDPR-compliant cookie consent management for React with script blocking and Google Consent Mode V2

Downloads

668

Readme

@meui-creative/cookies

GDPR-compliant cookie consent for React. Script blocking, Google Consent Mode V2, 5 preset layouts, dark mode, 9 languages, GPC support, WCAG 2.1 AA accessible.

Installation

npm install @meui-creative/cookies framer-motion lucide-react

framer-motion and lucide-react are only needed for the built-in UI presets. For headless usage (hook only), install just the main package.

How It Works

  1. You define your tracking scripts with type="text/plain" and data-cookie-category="..." — this prevents them from executing
  2. The library shows a consent banner to the user
  3. When the user accepts, scripts matching consented categories are unblocked and executed
  4. Google Consent Mode V2 signals are updated automatically

Quick Start (Next.js App Router)

1. Define your scripts in the layout

Mark each script with type="text/plain" and the appropriate data-cookie-category. The library will unblock them after consent.

// app/layout.tsx
import Script from 'next/script'
import { CookieConsent } from '@meui-creative/cookies'
import '@meui-creative/cookies/styles'

const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID
const GA_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID
const PIXEL_ID = process.env.NEXT_PUBLIC_FACEBOOK_PIXEL_ID

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="cs">
      <body>
        {/* Google Tag Manager — blocked until "analytics" consent */}
        {GTM_ID && (
          <Script
            id="gtm"
            type="text/plain"
            data-cookie-category="analytics"
            strategy="afterInteractive"
            dangerouslySetInnerHTML={{
              __html: `
                (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
                new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
                j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
                'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
                })(window,document,'script','dataLayer','${GTM_ID}');
              `,
            }}
          />
        )}

        {/* Google Analytics 4 — blocked until "analytics" consent */}
        {GA_ID && (
          <>
            <Script
              src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
              type="text/plain"
              data-cookie-category="analytics"
              strategy="afterInteractive"
            />
            <Script
              id="ga4"
              type="text/plain"
              data-cookie-category="analytics"
              strategy="afterInteractive"
              dangerouslySetInnerHTML={{
                __html: `
                  window.dataLayer = window.dataLayer || [];
                  function gtag(){dataLayer.push(arguments);}
                  gtag('js', new Date());
                  gtag('config', '${GA_ID}');
                `,
              }}
            />
          </>
        )}

        {/* Facebook Pixel — blocked until "marketing" consent */}
        {PIXEL_ID && (
          <Script
            id="facebook-pixel"
            type="text/plain"
            data-cookie-category="marketing"
            strategy="afterInteractive"
            dangerouslySetInnerHTML={{
              __html: `
                !function(f,b,e,v,n,t,s)
                {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
                n.callMethod.apply(n,arguments):n.queue.push(arguments)};
                if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
                n.queue=[];t=b.createElement(e);t.async=!0;
                t.src=v;s=b.getElementsByTagName(e)[0];
                s.parentNode.insertBefore(t,s)}(window, document,'script',
                'https://connect.facebook.net/en_US/fbevents.js');
                fbq('init', '${PIXEL_ID}');
                fbq('track', 'PageView');
              `,
            }}
          />
        )}

        {/* Cookie consent banner */}
        <CookieConsent preset="meui" language="cs" accentColor="#C0B7A7" />

        {children}
      </body>
    </html>
  )
}

2. That's it

No integrations prop, no auto-injection. You control your scripts, the library controls consent.

Script & Iframe Blocking

Any element with type="text/plain" and data-cookie-category is blocked until the user consents to that category:

<!-- External script — blocked until "analytics" consent -->
<script type="text/plain" data-cookie-category="analytics" src="https://example.com/analytics.js"></script>

<!-- Inline script -->
<script type="text/plain" data-cookie-category="marketing">
  console.log('Runs only after marketing consent');
</script>

<!-- Iframes — src is stripped until consent -->
<iframe data-cookie-category="marketing" src="https://www.youtube.com/embed/VIDEO_ID"></iframe>

A MutationObserver handles dynamically added elements too.

Categories: strictly-necessary (always on), functional, analytics, marketing

Entry Points

| Import | What you get | |--------|-------------| | @meui-creative/cookies | CookieConsent component + useCookieConsent hook + API | | @meui-creative/cookies/headless | Hook + API only (zero UI dependencies) | | @meui-creative/cookies/presets | Individual preset components | | @meui-creative/cookies/styles | CSS stylesheet |

Design Presets

default — Classic Card

Blue (#3b82f6), bottom-left. Clean card with cookie icon.

<CookieConsent />

meui — Compact

Green (#4A6953), bottom-left. Pill-shaped buttons, inline expandable settings.

<CookieConsent preset="meui" />

minimal — Bottom Bar

Black (#000), full-width bottom bar.

<CookieConsent preset="minimal" />

card — Accent Card

Violet (#8b5cf6), bottom-right. Stacked buttons.

<CookieConsent preset="card" />

sidebar — Full-Height Panel

Emerald (#10b981), right sidebar slide-in with trust indicator.

<CookieConsent preset="sidebar" />

Customization

<CookieConsent
  preset="default"
  accentColor="#ec4899"
  borderRadius="sharp"       // 'rounded' | 'sharp'
  language="cs"               // 'en' | 'cs' | 'de'
  descriptionMode="long"     // 'short' | 'long'
  mode="strict"               // 'strict' (closing = decline) | 'soft'
  showSettingsButton={true}
  closeButton={true}
  showRejectLink={true}
  version="1.0.0"             // Bump to force re-consent
  ttl={365}                    // Consent expiry in days
  domain=".example.com"       // Cookie domain for subdomains
  style={{
    position: 'top-right',   // 'bottom-left' | 'bottom-center' | 'bottom-right' | 'top-left' | 'top-center' | 'top-right'
    maxWidth: 'lg',           // 'sm' | 'md' | 'lg' | 'xl'
    spacing: 'comfortable',   // 'compact' | 'normal' | 'comfortable'
    background: '#ffffff',
    shadow: '2xl',            // 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
    border: { width: '2', color: '#333' },
    fontSize: { heading: 'lg', body: 'sm' },
    button: { shape: 'pill', size: 'lg' },
    animation: { direction: 'up', duration: 0.3, enabled: true },
  }}
  links={{ privacyPolicy: '/privacy', cookiePolicy: '/cookies' }}
  onAccept={(choices) => console.log('Accepted:', choices)}
  onDecline={() => console.log('Declined')}
  onChange={(choices) => console.log('Changed:', choices)}
  onReady={() => console.log('Loaded from storage')}
  onBlock={(src) => console.log('Script unblocked:', src)}
  onVersionChange={(prev) => console.log('Version changed from:', prev)}
  debug={false}
/>

Dark Mode

// Explicit dark mode
<CookieConsent theme="dark" />

// Follow system preference (prefers-color-scheme)
<CookieConsent theme="auto" />

// Default: light
<CookieConsent theme="light" />

All presets fully support dark mode. Colors adapt automatically based on the theme.

Global Privacy Control (GPC)

The library automatically detects the navigator.globalPrivacyControl signal. When detected, it auto-declines all non-essential cookies without showing a banner.

// Enabled by default
<CookieConsent respectGPC={true} />

// Disable if needed
<CookieConsent respectGPC={false} />

GPC is legally required in California (CCPA), Colorado, and Connecticut.

Cookie Auto-Clear

When a user rejects a category, automatically delete cookies set by that category:

<CookieConsent
  customCategories={[
    {
      id: 'analytics',
      label: 'Analytics',
      description: 'Help us improve',
      required: false,
      autoClear: {
        cookies: [
          { name: '_ga' },
          { name: '_gid' },
          { name: '_ga_XXXXXXX', domain: '.example.com' },
        ],
      },
    },
  ]}
/>

Consent Record Logging

For GDPR Article 7(1) proof-of-consent, send consent records to your server:

<CookieConsent
  onConsentRecord={(record) => {
    // record: { version, timestamp, choices, userAgent, language, consentMethod, sourceUrl, gpcSignal }
    fetch('/api/consent-log', {
      method: 'POST',
      body: JSON.stringify(record),
    })
  }}
/>

Languages

Built-in: English, Czech, German, French, Spanish, Italian, Polish, Dutch, Slovak.

<CookieConsent language="fr" />

Custom translations override any built-in text:

<CookieConsent
  language="en"
  customTexts={{
    heading: 'Cookie Notice',
    acceptButton: 'I agree',
    declineButton: 'No thanks',
  }}
/>

Accessibility (WCAG 2.1 AA)

  • role="dialog" + aria-modal on settings modal
  • aria-labelledby + aria-describedby linking heading and description
  • role="region" + aria-label on banner
  • Full focus trap in modal (Tab/Shift+Tab cycle, no escape)
  • Focus restoration on modal close
  • Scroll lock when modal is open
  • role="checkbox" + aria-checked on category toggles
  • Keyboard navigation (Enter/Space to toggle, Escape to close)

Google Consent Mode V2

The hook/component pushes a consent: 'default' (all denied) on init and a consent: 'update' on every choice, via the canonical gtag(){ dataLayer.push(arguments) } form so GTM/gtag.js actually registers it. The update sets every signal explicitly (granted/denied), so a decline or a preference change genuinely denies — it never leaves a revoked category stuck as granted.

Requires ≥ 7.0.2. Earlier versions pushed commands as a plain array (ignored by GTM) and/or pushed a partial update (revoked categories not denied).

Category → signal mapping:

| Category | Consent Mode signals granted on opt-in | |----------|----------------------------------------| | strictly-necessary | functionality_storage | | functional | functionality_storage, personalization_storage | | analytics | analytics_storage | | marketing | ad_storage, ad_user_data, ad_personalization | | (always) | security_storage |

Two integration modes — pick one per tag

Basic — block the tag until consent. Mark the GTM/GA/Pixel script with type="text/plain" + data-cookie-category. The library unblocks it on consent. Nothing else to do — the hook's default/update run fine because the tag is blocked anyway.

Advanced — GTM loads on every visit (cookieless pings). Drop the type="text/plain" so GTM always loads, and gate individual tags inside GTM via Consent Mode. ⚠️ In this mode you must set the consent: 'default' synchronously in <head>, before GTM — the hook sets its default in a useEffect (after hydration), which races GTM. Then set window.__meui_consent_mode_initialized__ = true in that same head script so the hook does not push a second, late default. The hook still pushes the update on the user's choice.

// app/layout.tsx — inside <head>, BEFORE the GTM snippet.
// Region-scoped (Google best practice): granted worldwide, denied for EEA/UK/CH.
<script
  id="consent-mode-default"
  dangerouslySetInnerHTML={{
    __html:
      `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}` +
      `gtag('consent','default',{ad_storage:'granted',ad_user_data:'granted',` +
      `ad_personalization:'granted',analytics_storage:'granted',` +
      `functionality_storage:'granted',personalization_storage:'granted',security_storage:'granted'});` +
      `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,` +
      `region:['AT','BE','BG','HR','CY','CZ','DK','EE','FI','FR','DE','GR','HU','IE','IT','LV',` +
      `'LT','LU','MT','NL','PL','PT','RO','SK','SI','ES','SE','IS','LI','NO','GB','CH']});` +
      `gtag('set','url_passthrough',true);gtag('set','ads_data_redaction',true);` +
      `window.__meui_consent_mode_initialized__=true;`,
  }}
/>

A single worldwide denied default is also valid (simpler, just drop the first granted default and the region array) — region scoping only preserves measurement where consent isn't legally required.

Optional configuration (component / basic mode)

For the bundled CookieConsent component or basic mode, the same options are available on the hook config (region, urlPassthrough, adsDataRedaction):

<CookieConsent
  consentModeOptions={{
    region: ['EU', 'US-CA'],
    urlPassthrough: true,
    adsDataRedaction: true,
  }}
/>

GPC + headless: the bundled CookieConsent component honours Global Privacy Control (respectGPC). The headless hook does not — wire it yourself (see Headless Mode below).

Headless Mode

Use the hook only, build your own UI:

import { useCookieConsent } from '@meui-creative/cookies/headless'

function MyBanner() {
  const {
    consent,        // { 'strictly-necessary': true, analytics: false, ... }
    hasConsent,     // Has user made any choice?
    isReady,        // Initialization complete?
    categories,     // CategoryDefinition[]
    acceptAll,
    declineAll,
    setConsent,     // Granular: setConsent({ analytics: true, marketing: false })
    revokeConsent,
    exportConsent,
    showSettings,
  } = useCookieConsent({ version: '1.0.0', language: 'en' })

  if (!isReady || hasConsent) return null

  return (
    <div>
      <p>We use cookies</p>
      <button onClick={acceptAll}>Accept All</button>
      <button onClick={declineAll}>Decline</button>
    </div>
  )
}

The headless hook does not auto-handle GPC (only the bundled CookieConsent component does). If you want it in a custom banner, add it yourself:

// Auto-decline non-essential when the browser sends Global Privacy Control and
// the visitor hasn't chosen yet (legally binding in CA/CO/CT, USA).
useEffect(() => {
  if (!isReady || hasConsent) return
  const nav = navigator as Navigator & { globalPrivacyControl?: boolean }
  if (nav.globalPrivacyControl === true) declineAll()
}, [isReady, hasConsent, declineAll])

Re-opening settings from a footer link: showCookieSettings() (or the hook's showSettings) dispatches the meui:settings:opened window event — a custom banner must listen for it to re-open.

Programmatic API

import { showCookieSettings, revokeConsent, exportConsent } from '@meui-creative/cookies'

// Open settings modal from anywhere (e.g. footer link)
showCookieSettings()

// Revoke all consent
revokeConsent()

// Export consent record for GDPR data subject requests
const record = exportConsent()
// { version, timestamp, choices, userAgent, language, consentMethod, sourceUrl }

Events

window.addEventListener('meui:consent:given', (e) => {
  console.log('Consent given:', e.detail.consent)
})

window.addEventListener('meui:consent:revoked', () => {
  console.log('Consent revoked')
})

window.addEventListener('meui:consent:version-changed', (e) => {
  console.log('Re-consent needed, previous version:', e.detail.previousVersion)
})

Storage

  • localStorage (meui_cookie_consent) — Full audit record: version, timestamp, choices, userAgent, language, consentMethod, sourceUrl
  • Cookie (meui_cookie_consent) — Comma-separated list of consented categories (for server-side reading)

License

MIT