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

cookie-app

v2.0.2

Published

Quebec Law 25 (Bill 64) cookie consent for React & Next.js. Script blocking, Google Consent Mode v2, 3 banner styles, bilingual, zero dependencies.

Readme

cookie-app

Quebec Law 25 (Bill 64) cookie consent for React & Next.js 15.

Script blocking, Google Consent Mode v2, 3 banner styles, bilingual (FR/EN), zero dependencies.

Converted from the Loi 25 Quebec WordPress plugin by Rayels Consulting.


Features

  • Zero config — works out of the box with sensible defaults
  • Script Vault — blocks tracking scripts until consent is granted
  • Google Consent Mode v2 — full compliance with all 7 consent types, wait_for_update, ads_data_redaction, url_passthrough, and region-scoped defaults
  • Synchronous head scriptgetConsentModeScript() helper for correct tag ordering
  • 3 banner styles — full-width bar, centered popup, corner widget
  • Glassmorphism — modern frosted glass effect
  • Bilingual — French (default) and English with auto-detection
  • Custom text — override every string in both languages
  • Brand color — match your website's design
  • Consent expiry — auto re-ask after configurable days
  • Re-consent button — floating cookie button to change consent
  • Smooth animations — slide or fade transitions
  • Custom CSS — full styling control
  • Accessible — keyboard navigation (Escape = reject), ARIA labels, focus management
  • SSR-safe — works with Next.js 15 App Router and Server Components
  • TypeScript — full type definitions included
  • Tiny — zero external dependencies, under 10KB

Installation

npm install cookie-app
yarn add cookie-app
pnpm add cookie-app

Quick Start

Next.js 15 (App Router)

// app/layout.tsx
import { CookieConsent } from "cookie-app";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='fr'>
      <body>
        {children}
        <CookieConsent />
      </body>
    </html>
  );
}

That's it! The banner appears automatically for new visitors with French defaults.


Google Consent Mode v2 (Compliant Setup)

For full compliance with the official Google documentation, you need two parts:

  1. A synchronous inline script in <head> that sets consent defaults before Google tags load
  2. The <CookieConsent> component that sends consent('update', ...) when the user interacts

Recommended Setup

// app/layout.tsx (Next.js 15 App Router)
import { CookieConsent, getConsentModeScript } from "cookie-app";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='fr'>
      <head>
        {/* 1. Consent defaults — MUST come before the Google tag */}
        <script dangerouslySetInnerHTML={{ __html: getConsentModeScript() }} />

        {/* 2. Google tag (gtag.js) — loads AFTER consent defaults are set */}
        <script
          async
          src='https://www.googletagmanager.com/gtag/js?id=G-XXXXX'
        />
        <script
          dangerouslySetInnerHTML={{
            __html: `
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', 'G-XXXXX');
        `,
          }}
        />
      </head>
      <body>
        {children}

        {/* 3. Consent banner — handles consent('update') on user choice */}
        <CookieConsent
          consentMode
          adsDataRedaction
          urlPassthrough
          lang='auto'
          style='popup'
          theme='dark'
          privacyUrl='/privacy'
        />
      </body>
    </html>
  );
}

What this does

| Step | Timing | What happens | | ------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | getConsentModeScript() | Synchronous in <head> | Sets all consent types to denied with wait_for_update: 500ms. For returning users who previously accepted, immediately calls consent('update', granted). | | Google tag loads | After consent defaults | Tags see the default consent state and behave accordingly. | | <CookieConsent> mounts | After hydration | Also sets defaults via useEffect as a fallback, and applies ads_data_redaction / url_passthrough. | | User clicks Accept/Reject | On interaction | Calls consent('update', ...) with granted or denied for all 4 tracking types. |

getConsentModeScript() Options

getConsentModeScript({
  // Override defaults (all default to 'denied' except functional types)
  ad_storage: "denied",
  ad_user_data: "denied",
  ad_personalization: "denied",
  analytics_storage: "denied",
  functionality_storage: "granted", // default: 'granted'
  personalization_storage: "granted", // default: 'granted'
  security_storage: "granted", // default: 'granted'

  // How long Google tags wait for consent update (ms)
  wait_for_update: 500, // default: 500

  // Scope defaults to specific regions (ISO 3166-2)
  region: ["CA-QC"],

  // Redact ad click identifiers when ad_storage is denied
  ads_data_redaction: true,

  // Pass GCLID/DCLID through URL params when cookies denied
  url_passthrough: true,

  // Must match expiryDays on <CookieConsent>
  expiry_days: 365,
});

Consent Types Managed

| Consent Type | Default | Description | | ------------------------- | --------- | ------------------------------------------------ | | ad_storage | denied | Advertising cookie storage | | ad_user_data | denied | User data for advertising | | ad_personalization | denied | Personalized advertising | | analytics_storage | denied | Analytics cookie storage | | functionality_storage | granted | Functionality (e.g. language settings) | | personalization_storage | granted | Personalization (e.g. video recommendations) | | security_storage | granted | Security (e.g. authentication, fraud prevention) |


Full Example

import { CookieConsent } from "cookie-app";

<CookieConsent
  lang='auto'
  position='bottom'
  theme='dark'
  style='popup'
  glassmorphism
  brandColor='#7c3aed'
  privacyUrl='/politique-de-confidentialite'
  expiryDays={365}
  showReconsent
  showIcon
  animation='slide'
  consentMode
  adsDataRedaction
  urlPassthrough
  consentModeRegion={["CA-QC"]}
  scripts={`
    <!-- Google Analytics -->
    <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXX"></script>
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('js', new Date());
      gtag('config', 'G-XXXXXX');
    </script>
  `}
  textsFr={{
    title: "Respect de votre vie privée",
    message: "Nous utilisons des cookies pour améliorer votre expérience.",
    accept: "Tout accepter",
    reject: "Refuser",
  }}
  textsEn={{
    title: "Your Privacy Matters",
    message: "We use cookies to improve your experience.",
    accept: "Accept All",
    reject: "Reject",
  }}
  onConsent={(level) => {
    console.log("User chose:", level); // 'all' or 'necessary'
  }}
/>;

Props

| Prop | Type | Default | Description | | ------------------- | ------------------------------- | --------------------------------- | ------------------------------------------------------------------------- | | lang | 'fr' \| 'en' \| 'auto' | 'fr' | Banner language. 'auto' detects from navigator.language. | | position | 'top' \| 'bottom' | 'bottom' | Banner position (bar & corner styles). | | theme | 'light' \| 'dark' | 'light' | Color theme. | | style | 'bar' \| 'popup' \| 'corner' | 'bar' | Banner display style. | | glassmorphism | boolean | false | Frosted glass effect. | | privacyUrl | string | '/politique-de-confidentialite' | Privacy policy link URL. | | poweredBy | boolean | false | Show "Powered by Pomme&Olive" link. | | brandColor | string | '#1d4ed8' | Accept button & reconsent button color. | | expiryDays | number | 365 | Days before consent expires. | | showReconsent | boolean | true | Show floating reconsent button. | | animation | 'slide' \| 'fade' | 'slide' | Animation type. | | showIcon | boolean | true | Show cookie emoji in banner & button. | | customCss | string | '' | Custom CSS targeting #loi25-banner. | | textsFr | ConsentTexts | — | French text overrides. | | textsEn | ConsentTexts | — | English text overrides. | | onConsent | (level: ConsentLevel) => void | — | Callback when user consents. | | consentMode | boolean | false | Enable Google Consent Mode v2. | | adsDataRedaction | boolean | false | Redact ad click identifiers when ad_storage is denied. | | urlPassthrough | boolean | false | Pass GCLID/DCLID through URL params when cookies denied. | | consentModeRegion | string[] | — | ISO 3166-2 region codes to scope consent defaults (e.g. ['CA-QC']). | | waitForUpdate | number | 500 | Milliseconds Google tags wait for consent update before firing. | | scripts | string | '' | HTML of tracking scripts to block until consent. | | reloadOnConsent | boolean | false | Reload page after accepting (for scripts that need page-start execution). |


useConsent Hook

Read and manage consent state from any component. SSR-safe.

"use client";

import { useConsent } from "cookie-app";

export function AnalyticsLoader() {
  const { consent, hasConsent, resetConsent, setConsent } = useConsent();

  if (hasConsent && consent === "all") {
    return <p>Analytics are enabled.</p>;
  }

  return (
    <div>
      <p>No analytics consent.</p>
      <button onClick={resetConsent}>Change cookie preferences</button>
    </div>
  );
}

Return Values

| Property | Type | Description | | -------------- | ------------------------------- | ------------------------------------------- | | consent | 'all' \| 'necessary' \| null | Current consent level. | | hasConsent | boolean | Whether valid (non-expired) consent exists. | | resetConsent | () => void | Clear consent and trigger banner. | | setConsent | (level: ConsentLevel) => void | Set consent programmatically. |


Script Vault

The killer feature. Paste your tracking scripts into the scripts prop and they are automatically blocked until the user clicks "Accept All".

<CookieConsent
  scripts={`
    <!-- Meta Pixel -->
    <script>
      !function(f,b,e,v,n,t,s){...}(window,document,'script',
      'https://connect.facebook.net/en_US/fbevents.js');
      fbq('init', '123456789');
      fbq('track', 'PageView');
    </script>

    <!-- Hotjar -->
    <script>
      (function(h,o,t,j,a,r){...})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
    </script>
  `}
/>

Scripts are dynamically injected into <head> after consent. For scripts that must run at page load (e.g., GTM), set reloadOnConsent to trigger a page reload.


Exported Constants

import {
  STORAGE_KEY, // 'loi25-consent'
  STORAGE_DATE_KEY, // 'loi25-consent-date'
  CONSENT_CHANGE_EVENT, // 'loi25-consent-change'
  DEFAULT_BRAND_COLOR, // '#1d4ed8'
  DEFAULT_EXPIRY_DAYS, // 365
  DEFAULT_WAIT_FOR_UPDATE, // 500
  DEFAULT_TEXTS, // { fr: {...}, en: {...} }
} from "cookie-app";

Vanilla JS API

You can also check consent outside of React:

localStorage.getItem("loi25-consent"); // 'all' | 'necessary' | null

TypeScript

All types are exported:

import type {
  ConsentLevel,
  Language,
  BannerStyle,
  BannerPosition,
  BannerTheme,
  Animation,
  ConsentTexts,
  CookieConsentProps,
  ConsentState,
  ConsentModeDefaults,
} from "cookie-app";

Banner Styles

Bar (default)

Full-width bar fixed to top or bottom of the viewport.

Popup

Centered modal with a semi-transparent overlay backdrop.

Corner

Compact widget anchored to the bottom-right (or top-right) corner.


Migration from WordPress Plugin

If you're migrating from the WordPress plugin:

| React Prop | | ------------------------------------- | | lang | | position | | theme | | style | | glassmorphism | | privacyUrl | | poweredBy | | brandColor | | consentMode | | expiryDays | | showReconsent | | animation | | showIcon | | customCss | | scripts | | textsFr.title / textsEn.title | | textsFr.message / textsEn.message | | textsFr.accept / textsEn.accept | | textsFr.reject / textsEn.reject |

What's different:

  • No admin settings page (configuration is via props)
  • No dashboard stats widget (use onConsent callback to log to your own backend)
  • No database table (use onConsent for server-side logging)
  • No cache flushing (not needed in Next.js)
  • localStorage keys are identical -- consent carries over from the WordPress version

License

MIT -- Pomme&Olive, Montreal, Quebec.