Skip to content

Repository files navigation

@snowpact/snowcookie

Minimal, CNIL-friendly cookie-consent for React — one consent category, accept/refuse, re-openable, versioned 6-month persistence. No runtime dependency, no Tailwind: the banner ships its own standalone CSS and inherits your font.

Live Demo

What it does

  • Stores the choice (acceptance and refusal) in a first-party cookie for 182 days (~6 months, the CNIL ceiling before re-asking).
  • version bump → every visitor is re-prompted (use it when your tracker roster changes).
  • Malformed/unknown cookie → treated as pending, never throws.
  • SSR-safe: server HTML and first client render both see pending (no banner, no tracker), the cookie is read after mount — no hydration mismatch, no banner flash for returning visitors.
  • Non-blocking banner (role="dialog" aria-modal="false", no overlay, no focus trap), accept and refuse rendered with equal prominence — both CNIL requirements.
  • reopen() re-prompts without dropping the stored choice: the previous consent record stays valid until the user actually decides again.

Install

pnpm add @snowpact/snowcookie

Peer dependency: react >= 18.

Usage

import { SnowCookie, SnowCookieProvider, useSnowCookie } from '@snowpact/snowcookie';
import '@snowpact/snowcookie/styles.css';

CSS import order matters: import styles.css after your global/reset stylesheet (Tailwind preflight, normalize…). Resets target [type='button'] with the same specificity as the banner classes — imported first, the package CSS would lose the button styling coin-flip.

Wrap your whole app shell with the provider — including the footer if it hosts the "Manage cookies" button (reopen needs the context):

<SnowCookieProvider version={1} cookieName="my_app_cookie_consent">
  <App />
</SnowCookieProvider>

Gate your trackers and mount the banner anywhere below the provider:

const { isGranted } = useSnowCookie();

{isGranted && <MyAnalyticsScript />}
<SnowCookie
  title="Cookies et mesure d'audience"
  description="…"
  acceptLabel="Accepter"
  refuseLabel="Refuser"
  policyHref="/privacy"
  policyLabel="En savoir plus"
  onRefuse={handleRefuse}
/>

Labels are plain props: i18n stays in the host app, the package hardcodes no user-facing text.

"Manage cookies" footer link → re-prompt:

const { reopen } = useSnowCookie();
<button type="button" onClick={reopen}>Gérer les cookies</button>

Refusing after trackers already ran (read this)

Two things every consumer needs, learned the hard way:

  1. A tracker that already loaded keeps tracking until the page dies. Unmounting the script tag is not enough — reload the page when a refusal follows an acceptance in the same session.
  2. Trackers can rewrite their cookies between your click-time purge and the unload (GA4 does). Re-purge on every visit where the status is denied.

The complete recipe:

const TRACKER_COOKIE_PREFIXES = ['_ga', '_gid', '_clck', '_clsk', '_pk_'];

const { isGranted, status } = useSnowCookie();

// Latch: did a tracker run this session?
const wasGranted = useRef(false);
useEffect(() => {
  if (isGranted) wasGranted.current = true;
}, [isGranted]);

// Catch cookies rewritten after the click-time purge (GA4 notably).
useEffect(() => {
  if (status === 'denied') purgeCookies(TRACKER_COOKIE_PREFIXES);
}, [status]);

const handleRefuse = () => {
  purgeCookies(TRACKER_COOKIE_PREFIXES);
  if (wasGranted.current) window.location.reload();
};

purgeCookies is best-effort by design: it expires each matching cookie against the hostname and every parent domain (the only way to delete a _ga set on .example.com).

API

  • readConsent(cookieName, version) / writeConsent(cookieName, version, 'granted' | 'denied') / clearConsent(cookieName) — pure cookie semantics, usable without React.
  • purgeCookies(prefixes: string[]) — best-effort deletion of tracker cookies by name prefix.
  • SnowCookieProvider / useSnowCookie(){ status, isGranted, isPending, accept, refuse, reopen }.
  • <SnowCookie /> — the banner; renders null unless the choice is pending. Props: description, acceptLabel, refuseLabel, title?, policyHref?, policyLabel?, onAccept?, onRefuse?, className?.

Theming

Three CSS custom properties, overridable from any scope (defaults live in var() fallbacks, so :root just works):

:root {
  --snow-cookie-accent: #140b7c; /* buttons, link, icon — default: neutral #1f2937 */
  --snow-cookie-accent-fg: #ffffff; /* accept button text */
  --snow-cookie-z: 60;
}

The banner inherits the host font (font: inherit — set your font on body). Anything else (background, radius, spacing…) is overridable with plain CSS on the flat .snow-cookie-* classes — import your override after styles.css.

Next.js note

The package ships built ESM/CJS + types: no transpilePackages needed. Import the CSS from _app (Pages Router) or the root layout (App Router).

Browser support & limits

Plain document.cookie with max-age + expires + SameSite=Lax — every modern browser. Known limit shared by all client-side consent banners: Safari ITP caps JS-written cookies at ~7 days, so Safari users may be re-prompted weekly (legally harmless — worst case you re-ask).

Development

pnpm install
pnpm test
pnpm typecheck
pnpm build          # dist/: index.js (ESM) + index.cjs + index.d.ts + styles.css

Release (from main, clean tree, npm auth):

./scripts/release.sh patch|minor|major

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages