@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/react2. 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.cssInline 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 requiresstyle-srcto allow it — a hash, a nonce, or'unsafe-inline'. If you serve a strictstyle-src 'self', either add the block's hash to your policy or skipcritical.cssand keep loadingstyles.cssas an external stylesheet, which needs nostyle-srcexception at all. This SDK's own runtime theming works understyle-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 initand 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 whenregulationis"CCPA".<CookiePreferences />— the per-category preferences dialog.<CookieOptOut />— the CCPA opt-out dialog. Include it when usingregulation: "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]
categoriesandcommittedCategoriesare not interchangeable.categoriesis the live value — it changes on every dialog toggle, including ones the visitor never saved.committedCategoriesonly changes on a real decision (accept / reject / save / reset).Gate scripts, embeds, and trackers on
committedCategories. Gating oncategoriesmeans 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. Usecategoriesonly 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(); // booleanReact 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()andgetCookieYes()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: useuseConsent()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>readServerConsenttouches no browser API, so it's safe in any server runtime. Next.js App Router users get a wrapper that readscookies()for them —getServerConsent()from@cookieyes/nextjs/server.- Returns
nullwhen 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). Passingnullrenders exactly as it did before, so adding this is safe. initialConsentis a provider prop, never aninitCookieYesoption. 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 ofregion/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. Esccloses<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+Tabcycle only through its own controls. The banner does not take focus when it appears; the firstTabfrom 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, orEsc— returns focus to whatever control opened it. - Visible focus indicator: every interactive control has a
:focus-visibleoutline; 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
- Open an issue — bug reports and feature requests.
- Email — [email protected].
- Full documentation — configuration, migration, examples.
(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.cssship as a real stylesheet (import "@cookieyes/react/styles.css"), loaded via a<link>, not a<style>block —style-srcdoesn'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 withelement.style.setProperty(...)— direct CSSOM writes, not a generated<style>block — whichstyle-srcdoesn'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.
