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

@cookieyes/react

v0.11.0

Published

React adapter for the CookieYes consent SDK — components and hooks

Downloads

1,868

Readme


Free-tier note: the "Powered by CookieYes" attribution in the banner may not be removed on the free tier. Paid plans remove it.


Key features

  • Drop-in components — banner, preferences dialog, and recall button work out of the box, no wiring required.
  • Fully themeable — every color, radius, and font is a token you control, or override with your own CSS.
  • GDPR & CCPA — built-in opt-in (GDPR) and "Do Not Sell" opt-out (CCPA) flows.
  • Headless primitives — compose your own UI from low-level slots when the presets aren't enough.
  • Script gating — block analytics and ad scripts until consent is granted, with one component.
  • React hooks — read and change consent state anywhere in your component tree.
  • Tiny & tree-shakeable — TypeScript-first, one dependency (@cookieyes/core).

Prerequisites

  • Node.js ≥ 20
  • React ≥ 18 and React DOM ≥ 18 (peer dependencies)
  • A browser environment. For the Next.js App Router / Server Components, use @cookieyes/nextjs.

| Next.js | React | React DOM | Verified | Build + typecheck | SSR (HTML + cookie value) | Behaviour (jsdom) | |---|---|---|---|---|---|---| | 14.0.0 (declared floor) | 18.0.0 (declared floor) | 18.0.0 | 2026-08-27 | pass | pass | pass | | 15.5.24 | 18.3.1 | 18.3.1 | 2026-08-27 | pass | pass | pass | | 16.3.0 | 19.2.8 | 19.2.8 | 2026-08-27 | pass | pass | pass |

Verified by installing the exact versions above, building a real Next.js app, server-rendering it and asserting the banner's HTML (and a returning visitor's cookie value) is correct, then running behaviour assertions in jsdom via @cookieyes/test. Node 20, pnpm. See rationale for why this stops short of a real browser.

Not verified by this table:

  • Any Next.js/React version outside the rows above.
  • npm/yarn as the install client.
  • No real browser paint or hydration — SSR is asserted via server-rendered HTML/fetch only; client behaviour is asserted in jsdom, not a browser DOM (no Playwright).
  • Node, package-manager and module-resolution dimensions are not yet varied — every combination above ran on Node 20 / pnpm / moduleResolution=bundler.

Quick start

Get a working banner in under 5 minutes.

1. Install the package

npm install @cookieyes/react
pnpm add @cookieyes/react
yarn add @cookieyes/react
bun add @cookieyes/react

2. Configure the runtime and render the components

Both the initCookieYes call and the components must live in a "use client" module.

// components/consent-manager.tsx
"use client";

import {
  CookieBanner,
  CookiePreferences,
  RecallButton,
  initCookieYes,
} from "@cookieyes/react";
import "@cookieyes/react/styles.css";

initCookieYes({
  mode: "cookie-only",   // "cookie-only" = no backend needed | "self-hosted"
  regulation: "GDPR",    // "GDPR" | "CCPA"
  colorScheme: "system", // "light" | "dark" | "system"
});

export function CookieYesRoot() {
  return (
    <>
      <CookieBanner />
      <CookiePreferences />
      <RecallButton />
    </>
  );
}

3. Render it once near the root of your app — ideally as the first element (before your page content), so the banner is early in the DOM: announced first by screen readers and painted before the rest of the page.

The @cookieyes/react/styles.css import is required — the banner and dialogs ship no inline styling, so without it they render unstyled. Import it once per app, wherever your bundler picks up CSS imports.

import { CookieYesRoot } from "./components/consent-manager";

function App() {
  return (
    <>
      <CookieYesRoot />
      <YourApp />
    </>
  );
}

Optional: inline the banner's CSS, defer the rest

styles.css is about 25 KB and styles everything — banner, preferences dialog, opt-out flow, toggles, the revisit widget. If your bundler puts it in the critical path (the usual case for an app-root import), the banner is already styled on the first paint and you need nothing more.

If instead you want to keep that 25 KB off the critical path, there is a paint-critical subset containing only the rules needed to render the banner:

@cookieyes/react/critical.css

Inline it in <head> and load the full sheet without blocking render. The banner is then correctly styled in the first frame, and the dialog styles arrive before the visitor can open the dialog:

<head>
  <style>/* contents of @cookieyes/react/critical.css */</style>
  <link rel="stylesheet" href="/path/to/styles.css" media="print" onload="this.media='all'">
</head>

It is roughly 1.6 KB gzipped, and every rule in it is byte-identical to the same rule in styles.css, so the two never disagree about how the banner looks. Load styles.css as well — critical.css intentionally has no dialog, opt-out, toggle or widget styling.

Content Security Policy: inlining CSS in a <style> block requires style-src to allow it — a hash, a nonce, or 'unsafe-inline'. If you serve a strict style-src 'self', either add the block's hash to your policy or skip critical.css and keep loading styles.css as an external stylesheet, which needs no style-src exception at all. This SDK's own runtime theming works under style-src 'self' either way — it writes theme values through the CSSOM, never as inline style text.

4. Done. The banner appears on page load until the user acts. If it doesn't, see Troubleshooting.

Prefer zero manual setup? Run npx @cookieyes/cli init and the CLI wires all of this up for you.

Which API should I use?

useConsent() is the recommended way to read consent in React. See the shared decision tree if you're not sure which API applies to your situation — this package also exposes a handful of lower-level hooks (see Hooks) for specific edge cases.

Usage

initCookieYes(config) takes the canonical CookieYesConfig object — the same shape accepted by @cookieyes/core and @cookieyes/nextjs, copy-pasteable between them with zero edits. The full option reference (modes, theme, i18n, self-hosted persistence, callbacks) lives in Configuration.

| Option | Type | Notes | |--------|------|-------| | mode | "cookie-only" \| "self-hosted" | Required. cookie-only = zero network, consent stored in a cookie. self-hosted = sync to your backend. See Deprecated for the retired "offline" name. | | regulation | "GDPR" \| "CCPA" | Which regulation applies. Drives the banner variant. | | colorScheme | "light" \| "dark" \| "system" | Theme mode. | | theme | ThemeConfig | Color / radius / font tokens (see Theming). | | i18n | I18nConfig | Locale translation maps (see @cookieyes/translations). | | apiUrl / backend | string / ConsentBackend | Self-hosted persistence (mode: "self-hosted"). | | integrations | Integration[] | Consent-gated third-party scripts (Segment, custom scripts, …) via presets from @cookieyes/scripts. | | onConsentReady / onConsentUpdate | (state) => void | Lifecycle callbacks. |

Migrating from the deprecated createCookieYes() builder? See the migration guide.

Components

Presets (styled, drop-in)

  • <CookieBanner /> — the consent banner. Shows until the user acts; renders the CCPA "Do Not Sell" variant when regulation is "CCPA".
  • <CookiePreferences /> — the per-category preferences dialog.
  • <CookieOptOut /> — the CCPA opt-out dialog. Include it when using regulation: "CCPA".

Controls

  • <RecallButton /> — floating button to reopen preferences after the user has acted.

  • <GatedScript /> — registers a third-party script that only loads once its category is consented:

    <GatedScript
      id="gtm"
      src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"
      category="analytics"
      strategy="afterConsent" // "afterConsent" | "lazyOnce"
    />
  • <GatedFrame /> — blocks an iframe until its category is granted, showing a placeholder otherwise.

Headless primitives

For fully custom UIs, compose the slot namespaces Banner, Preferences, and OptOut (e.g. Banner.Root, Banner.AcceptAll, Preferences.Category). The presets are built from exactly these primitives.

Hooks

Read consent state:

const {
  consentId,           // string — stable id for this visitor's consent record
  hasActed,            // boolean — whether a real decision has been made
  categories,          // Record<string, boolean> — LIVE values, include unsaved dialog toggles
  committedCategories, // Record<string, boolean> — consent IN EFFECT. Gate on this
  regulation,          // "GDPR" | "CCPA"
  lastRenewed,         // number | undefined — timestamp of the last decision
  taxonomyHash,        // string | undefined — signature of the taxonomy consent was recorded under
  isPreferencesOpen,   // boolean
  isOptOutOpen,        // boolean
  reloadNotice,        // { required: boolean; reasons: string[] }
} = useConsent();

[!IMPORTANT] categories and committedCategories are not interchangeable. categories is the live value — it changes on every dialog toggle, including ones the visitor never saved. committedCategories only changes on a real decision (accept / reject / save / reset).

Gate scripts, embeds, and trackers on committedCategories. Gating on categories means a visitor who flips a switch in the preferences dialog and closes it without saving has, as far as your code is concerned, granted consent. Use categories only to drive the checkboxes in a custom preferences UI.

For gating a single category with fewer re-renders, prefer useConsentCategory(category), which reads the committed value.

Drive consent (accept/reject/save, open or close dialogs):

const {
  acceptAll, rejectAll, acceptSelected, save, updateCategory, reset,
  showPreferences, hidePreferences, showOptOut, hideOptOut,
} = useConsentActions();

In self-hosted mode each decision is sent as a consent record. Tell it where your UI is with useConsentActions("banner") (or "preferences", "optout"); without it, decisions are recorded as "api". The SDK's own components already do this.

Other hooks (each reads something useConsent() doesn't cover, so these aren't alternatives to it):

const regulation = useRegulation();          // "GDPR" | "CCPA"
const t = useTranslations();                 // active TranslationMap
const bannerVisible = useBannerVisibility();  // boolean
const prefsOpen = usePreferencesOpen();       // boolean
const optOutOpen = useOptOutOpen();           // boolean

React to consent changes — run your own code when a visitor grants or withdraws consent (load a script, sync a pixel, log the decision):

// "change" fires only when a category actually differs; "save" fires on every
// save. Pass { category } to scope it. Cleans up on unmount; no-op during SSR.
useOnConsentChange("change", ({ changedCategories }) => {
  if (changedCategories.includes("analytics")) loadAnalytics();
});

The listener also fires once on mount with the current state (isInitial: true), so late-mounting code still learns what the visitor already chose.

Low-level hooks

You shouldn't need these for a typical integration — each exists for a narrower situation than useConsent() / useConsentActions():

| Hook / callback | Use when | |---|---| | useConsentCategory(category) | You're gating one thing (e.g. an embed) and want to re-render only when that category changes, not on every consent update. | | useConsentRuntime() | You need direct access to the underlying runtime (manager, snapshot getters, script registration) — something neither useConsent() nor useConsentActions() exposes. | | getCookieYes() | You need imperative, non-hook access outside a component (event handlers, non-component modules). | | .onConsentReady(fn) (builder) | A one-time callback right after the initial state is known, rather than an ongoing subscription. | | .onConsentUpdate(fn) (builder) | Fires on every saved change only (not transient toggles), registered once at config time. For a dynamic subscribe/unsubscribe instead, use useConsentRuntime().manager.subscribe or core's consentStore.getState().subscribeToConsentChanges. |

Every hook that reads state is SSR-safe — it falls back to a stable fresh-visitor snapshot when no runtime is mounted, on the server included.

[!WARNING] useConsentRuntime() and getCookieYes() are the two exceptions: they throw. There is no meaningful fallback for "give me the runtime" when there isn't one, so both raise rather than hand back something fake:

[cookieyes] No runtime is registered. Call initCookieYes(...) in a 'use client' module before using hooks or components.

Registration is not guarded by an environment check, so whether a runtime exists during server rendering depends on whether your initCookieYes() module was evaluated for the tree being rendered — which makes this succeed on one route and throw on another. Do not read the runtime while server rendering: use useConsent() or a focused hook for state, and reach for the runtime only in code that runs after mount.

Rendering & selector contract

<CookieBanner /> is server-rendered: its markup is present in the initial HTML (first-byte paint) and on every load, before client JavaScript hydrates the interactive parts. It uses fixed positioning, so showing it never shifts page layout (no CLS), and it issues no network request on load (cookie-only mode makes zero requests; self-hosted mode only POSTs to your backend when the user accepts, rejects, or saves, or on a later load to resend a record your backend has not confirmed yet).

The following selectors are a stable, public contract — automated tooling and your own integrations may rely on them, and they will not change without a major-version bump and a regression test:

| Selector | Element | |----------|---------| | [data-cky-banner] | Canonical banner element — the visible card. Carries role="dialog". | | .cy-banner | The visible banner card (same element as above). Its bounding box equals what the user sees. | | .cy-banner-wrap | A logical grouping wrapper rendered with display: contents — it generates no box and is never the measured element. |

Theming

Pass a theme to initCookieYes. Theme values map to CSS custom properties applied directly to the elements (via CSSOM), so custom colors work even under a strict style-src CSP with no unsafe-inline/nonce — and you can override the same custom properties from your own stylesheet. This is separate from the base component styles, which ship as the external @cookieyes/react/styles.css you import once (see Quick start). See the theming reference in Configuration.

initCookieYes({
  mode: "cookie-only",
  theme: {
    primaryColor: "#6366F1",
    backgroundColor: "#ffffff",
    textColor: "#111827",
    mutedTextColor: "#6B7280",
    borderColor: "#E5E7EB",
    borderRadius: "8px",
    fontFamily: "'Inter', sans-serif",
    buttonVariant: "filled",        // "filled" | "outlined"
    widgetPosition: "bottom-right", // "bottom-right" | "bottom-left"
  },
});

Custom styling with CSS

Our styles are low-specificity and self-contained, so your app's CSS won't accidentally reshape the banner — and you can still override any part deliberately. Four ways, most reliable first.

1. Theme tokens — colours, radius, font. Set the --cy-* custom properties via the theme config, or in your own CSS. They win regardless of specificity, so they always apply:

| Token | theme key | Default | |---|---|---| | --cy-primary | primaryColor | #1863dc | | --cy-primary-hover | (derived) | color-mix(…) | | --cy-bg | backgroundColor | #ffffff | | --cy-text | textColor | #212121 | | --cy-muted | mutedTextColor | #6b7280 | | --cy-border | borderColor | #f4f4f4 | | --cy-radius | borderRadius | 6px | | --cy-font | fontFamily | system stack | | --cy-widget-bg | (fixed) | #0056a7 |

These names are a supported contract — a rename is a breaking change.

2. styles prop — an inline style on any part, guaranteed to win:

<CookieBanner styles={{ acceptAll: { borderRadius: 12 } }} />

3. classNames prop or your own CSS. Target parts by data-cy-part (toggles also carry data-cy-state="on" | "off"; state via native :hover / :focus-visible / :disabled). These tie with our specificity, so import your CSS after @cookieyes/react/styles.css:

<CookieBanner classNames={{ acceptAll: "bg-indigo-600 text-white" }} />
[data-cy-part="accept-all"]:hover { background: #15803d; }
[data-cy-part="toggle"][data-cy-state="on"] .cy-toggle-track { background: #16a34a; } /* checked */

Keys are typed (BannerPart / DialogPart / OptOutPart); the parts are also the CY_PART / CY_STATE constants:

| Component | data-cy-part | |---|---| | Banner | banner, title, description, actions, accept-all, reject-all, customise, do-not-sell, close, branding | | Preferences | overlay, dialog, title, close, intro, category, category-label, category-description, toggle, accept-all, reject-all, save, branding | | Opt-out | optout, title, message, confirm, close | | Recall button | recall | | Reload notice | reload-notice, reload-message, reload-dismiss |

4. asChild — replace our element with your own; we keep the behaviour (click action, data-cy-part, ref) and your className / style / handlers:

<Banner.AcceptAll asChild>
  <MyButton className="brand-btn" onClick={track}>Accept all</MyButton>
</Banner.AcceptAll>

Tailwind works with no !important — pass utilities via classNames, or point our tokens at yours:

[data-cy-part="banner"] { --cy-primary: var(--brand-500); }              /* match a design system */
.dark [data-cy-part="banner"] { --cy-bg: #0b1220; --cy-text: #e5e7eb; }  /* dark mode */

Translations (i18n)

Give the SDK your languages via i18n.messages. Each language can be partial — anything you leave out falls back to English:

import { fr } from "@cookieyes/translations/fr";

initCookieYes({
  mode: "cookie-only",
  i18n: {
    locale: "fr",                       // starting language (else the browser's, else English)
    messages: { fr, de: { acceptAll: "Alle akzeptieren" } }, // full or partial
  },
});

Read the text with useTranslations() — it re-renders when the language changes:

const t = useTranslations();
return <h2>{t.bannerTitle}</h2>;

Switch language live (no reload) and read language info with useLanguage():

const { language, direction, languages, setLanguage } = useLanguage();
// language: "fr" · direction: "ltr" | "rtl" · languages: what's loaded
<button onClick={() => setLanguage("fr")}>Français</button>

direction is handy for laying out a right-to-left language (Arabic, Hebrew) in a custom UI.

Loading a language on demand — instead of bundling every language, hand us a loader and we'll call it the first time that language is switched to:

initCookieYes({
  mode: "cookie-only",
  i18n: {
    loadLanguage: (tag) => import(`@cookieyes/translations/${tag}`).then((m) => m[tag]),
    // or fetch from your own URL: fetch(`/i18n/${tag}.json`).then((r) => r.json())
  },
});

Custom categories translate too — through the same messages, keyed by the category's id. The category's label/description in config is the default; a translation overrides it per language:

initCookieYes({
  mode: "cookie-only",
  categories: [{ id: "insights", label: "Shopping Insights" }], // default text
  i18n: { messages: { fr: { categories: { insights: { label: "Aperçus" } } } } },
});

Notes: the starting language is decided on each page load (we don't store the visitor's choice — persist it yourself if you want it remembered). A missing/failed language logs a developer warning and keeps the current one.

Region-based regulation (geo-detection)

Optionally choose the banner's regulation from where the visitor is. Fully optional — omit region and nothing changes; a manual regulation always wins.

initCookieYes({
  mode: "cookie-only",
  region: {
    // `detect` is YOUR function — return the visitor's region, however you get it.
    detect: () => window.__MY_REGION__,     // e.g. a value your server injected; "US-CA" | "DE" | undefined
    map: { "US-CA": "CCPA", DE: "GDPR" },   // you own the region → law mapping
    honorGpc: true,                          // default: honour the browser "do not sell" signal
    strictest: "GDPR",                       // used when unsure (default GDPR)
    debug: true,                             // log the decision to the console (local debugging)
  },
});

Rules: which banner shows is geo only — a detected region maps to your regulation; unknown or failed → the strictest (a required banner is never skipped); a manual regulation wins over detection (with a dev warning).

GPC (the browser's "do not sell" signal) never changes which banner shows. On a CCPA banner it starts the visitor opted out — non-required categories denied, so gated scripts/iframes don't run — until they choose otherwise. It's read in the browser, so it applies right after hydration. Set honorGpc: false to ignore it.

Inspect the decision:

const { region, regulation, source, confidence } = useRegion();
// source: "detected" | "strictest" | "manual"

Or set region.debug: true to print the same decision to the console at setup — a quick check without writing any component code.

For server-rendered correctness (the right banner in the first HTML per visitor, e.g. Next.js App Router), wrap your consent UI in <CookieYesProvider region={regionConfig}> — see the Next.js README.

detect must be synchronous. It returns a string, not a Promise — so you cannot await inside it. You pass a region you already have (e.g. one your server injected into the page). For an async IP lookup, fetch it first, then init:

const region = await fetch("/my-geo").then((r) => r.text()); // resolve it first
initCookieYes({ mode: "cookie-only", region: { detect: () => region, map } });

Hosting headers like Cloudflare CF-IPCountry or Vercel x-vercel-ip-country-region only exist server-side — reading those for you is the Next.js integration (below). In self-hosted mode the detected region is included on the consent-log payload (region).

Returning visitors — no banner flash (SSR)

If you server-render, the server has no idea whether a visitor already chose, so it renders the banner for everyone and the client removes it after hydration. A returning visitor sees the banner appear and then vanish.

Read their decision from the request and hand it to the provider — then the banner is never in their HTML at all:

// On the server, wherever you have the request:
import { readServerConsent } from "@cookieyes/react";

const initialConsent = readServerConsent(request.headers.get("cookie") ?? "", {
  regulation: "GDPR",
});
// Pass it into the tree:
<CookieYesProvider regulation="GDPR" initialConsent={initialConsent}>
  <CookieBanner />
</CookieYesProvider>
  • readServerConsent touches no browser API, so it's safe in any server runtime. Next.js App Router users get a wrapper that reads cookies() for them — getServerConsent() from @cookieyes/nextjs/server.
  • Returns null when the banner should show: a first-time visitor, a cookie recording no choice yet, a corrupt cookie, or one written against a different category taxonomy (which the client re-requests too). Passing null renders exactly as it did before, so adding this is safe.
  • initialConsent is a provider prop, never an initCookieYes option. The consent runtime is a module-level singleton shared across concurrent server requests, so per-visitor state stored there would leak between visitors. The same is true of region/regulation — that's why the provider exists.
  • The server render and the first hydration render read the same value, so there's no hydration mismatch; the real cookie is read on the client after commit and agrees.

Accessibility

Scope of this section: keyboard operability, focus management, screen-reader labelling, and reduced motion for <CookieBanner />, <CookiePreferences />, <CookieOptOut />, <RecallButton /> and <ReloadNotice />. This is not a "WCAG 2.1 AA compliant" claim — things outside this scope (color contrast, text resizing, and anything in your own custom theme/content) aren't covered and shouldn't be assumed to be.

Keyboard behavior

If you're building a custom UI on the headless primitives (Banner, Preferences, OptOut), this is the behavior to preserve:

  • Tab order follows DOM order in every preset — e.g. Preferences goes Close → category toggles → Reject All → Save → Accept All → branding link. <CookieBanner />, <CookiePreferences /> and <CookieOptOut /> are all modal (aria-modal="true") and trap focus.
  • Esc closes <CookiePreferences /> and <CookieOptOut />. It does not close the banner: the banner is something the visitor answers, not dismisses.
  • Focus trap: while the banner or a dialog is open, Tab / Shift+Tab cycle only through its own controls. The banner does not take focus when it appears; the first Tab from the page moves focus onto its first control.
  • Focus management on open/close: opening a dialog moves focus into it (onto the dialog element itself, which carries its aria-label); closing it — via Save, Cancel, or Esc — returns focus to whatever control opened it.
  • Visible focus indicator: every interactive control has a :focus-visible outline; none of this relies on the browser's default styling.

Reduced motion

All entrance/exit animations (banner slide-in, dialog fade/slide, the recall button's pop-in) are removed under prefers-reduced-motion: reduce — every element still appears and works identically, just without motion.

Automated testing

axe-core runs in CI (src/__tests__/a11y.test.tsx) and fails the build on any violation. Coverage is four cases across three components: <CookieBanner /> in both GDPR and CCPA modes, <CookiePreferences /> open, and <CookieOptOut /> open.

Caveat: this runs under jsdom, not a real browser — it catches structural/ARIA regressions (missing accessible names, wrong roles, broken labelling) but can't evaluate layout- or paint-dependent rules like color contrast. It's a regression net, not a substitute for manual testing.

Scope vs. coverage: <RecallButton /> and <ReloadNotice /> are in the scope stated above but have no axe coverage — their labelling is covered by other tests, but the automated accessibility suite does not run against them. Stated so the scope isn't read as a stronger guarantee than the suite gives. See Accessibility.

Manual testing

Keyboard and focus-management behavior above was verified by hand across desktop/tablet/mobile viewports and light/dark color schemes, and the labelling behavior was verified with VoiceOver. If you find something that doesn't sound right with your own screen reader, please open an issue.

The builder — createCookieYes()

The createCookieYes() builder is deprecated but still supported — new code should prefer the initCookieYes(config) object API above. If you're maintaining an existing integration, the builder configures the same runtime:

| Method | Purpose | |--------|---------| | .mode("cookie-only" \| "self-hosted") | Required. Cookie-only vs. synced to your backend. See Deprecated for the retired "offline" name. | | .regulation("GDPR" \| "CCPA") | Which regulation applies. | | .colorScheme("light" \| "dark" \| "system") | Theme mode. | | .theme(themeConfig) | Color / radius / font tokens. | | .i18n({ messages }) | Provide locale translation maps. | | .backend(adapter) / .backendURL(url) | Self-hosted persistence. | | .apiKey(key) | Optional auth key. | | .blockNetwork(config) | Block network requests (fetch/XHR/sendBeacon) until consent. See how script blocking works and what it costs. | | .categories([...]) | Define your own category taxonomy instead of the built-in five. See core: consent categories. | | .integrations([...]) | Built-in vendor stop-handlers — e.g. { vendor: "meta" } — the deprecated builtInIntegrations path. For new consent-gated scripts, pass integrations to initCookieYes(config) with a preset from @cookieyes/scripts. See core: stopping tracking. | | .customStopHandlers([...]) | Stop your own scripts on revoke (clean stop(), or needsReload: true). | | .reloadOnRevoke(true) | Legacy, off by default. Full page reload on revoke — erases what the visitor was doing. Prefer .integrations(...). | | .onConsentReady(fn) / .onConsentUpdate(fn) | Low-level lifecycle callbacks — see Hooks. | | .mount() | Build and register the runtime. |

If you use any reload-only integration (TikTok, LinkedIn, Hotjar, Segment, or a customStopHandlers entry marked needsReload), you must render <ReloadNotice /> alongside <CookieBanner />. It's the prompt shown when a tool that was running is revoked and can be fully stopped only by reloading — a dismissible, screen-reader-announced (role="alert") message inviting, never forcing, a reload. It appears only on a genuine revoke (a category that was granted, then denied — not a first-time reject), renders nothing until then, and stays dismissed until a new revoke needs it; wording is customizable via .i18n(...) (reloadNotice.*).

⚠️ Without it, a revoke that needs a reload has no way to tell the visitor — and that tool keeps running. (Tools that stop cleanly — Google Analytics/Tag Manager via Consent Mode, and Meta — don't need it.) For a fully custom notice, read the state with useReloadNotice() instead.

Deprecated: mode: "offline"

"offline" was renamed to "cookie-only" — same behavior, clearer name. It still works today and logs a one-time console warning, and will be removed 3 releases from now.

 createCookieYes()
-  .mode("offline")
+  .mode("cookie-only")
   .mount();

Troubleshooting

The banner doesn't appear. Make sure initCookieYes(...) runs inside a "use client" module and <CookieBanner /> is rendered near the root. The banner only shows while the user hasn't acted — clear the cookieyes-consent cookie and reload during testing.

I get a "use client" or hydration-mismatch error. Both the initCookieYes(...) call and the components must be in a "use client" file. On Next.js App Router, use @cookieyes/nextjs (pre-marked "use client"). The SSR banner carries your configured regulation so server and client render the same markup — passing different regulations between renders causes a mismatch.

My consent choice doesn't persist. Consent is stored in the cookieyes-consent cookie (SameSite=Lax, path=/). Check it isn't blocked by a browser privacy setting or extension, and that you aren't calling resetCookieYes() on every render.

Still stuck? Open an issue.

Community & support

(A community chat channel is on the roadmap.)

Contributing

Contributions are welcome. Read our Contributing Guidelines and Code of Conduct, then fork the repo, create a feature branch, and open a pull request.

Security

If you believe you've found a security vulnerability, please do not open a public issue. Follow our Security Policy and use GitHub's private vulnerability reporting.

Content Security Policy

The banner, dialogs, and theme colors all work under a strict style-src policy with no unsafe-inline and no nonce — nothing here writes CSS text into the page.

  • Layout, animations, and everything else in cookieyes.css ship as a real stylesheet (import "@cookieyes/react/styles.css"), loaded via a <link>, not a <style> block — style-src doesn't restrict where a real stylesheet is fetched from as long as it's same-origin (which it is, once bundled by your own build).
  • .theme(...) colors are applied with element.style.setProperty(...) — direct CSSOM writes, not a generated <style> block — which style-src doesn't govern at all, under any policy.

A minimal policy line that works out of the box, with no CookieYes-specific allowance needed:

Content-Security-Policy: style-src 'self'

If something unrelated to CookieYes still gets blocked (your own inline styles, a third-party script), the SDK listens for the browser's securitypolicyviolation event and logs a console warning explaining what was blocked, rather than failing silently.

License

MIT — see LICENSE. The "Powered by CookieYes" attribution in the banner may not be removed on the free tier.