@bulut5/ck
v0.6.0
Published
Lightweight cookie-consent banner with Google Consent Mode v2 + Microsoft UET signaling.
Readme
ck
A lightweight, dependency-free cookie-consent banner for React that signals Google Consent Mode v2 and Microsoft UET. Default-deny, granular categories, SSR-safe, themeable with Tailwind.
Install
npm i @bulut5/ckUsage
import { ConsentProvider, CookieConsent } from "@bulut5/ck";
const config = {
brand: "example",
locale: "en-GB",
privacyPolicyUrl: "/privacy",
copy: { /* … see BrandConfig */ },
};
export default function App() {
return (
<ConsentProvider config={config}>
{/* your app */}
<CookieConsent />
</ConsentProvider>
);
}Prompt timing and appearance
By default the card opens on mount for an undecided visitor. Two options change when and how it appears, without changing what it asks or how the decision is stored.
export const consentConfig: BrandConfig = {
// ...
prompt: "onInteraction", // "immediate" (default) | "onInteraction" | "manual"
};
<CookieConsent appearance="strip" /> // "card" (default) | "strip"prompt
immediate— on mount, as before.onInteraction— 400 ms after the visitor's first pointer, key or scroll input, so it never lands under the finger that just tapped. Consent Mode stays at its default until the decision.manual— never opens by itself. The page callsreopen()fromuseConsent()when it wants the prompt.
In every mode the floating badge is rendered whenever the prompt is not showing, so settings are always one tap away.
appearance
card— the bottom-right card.strip— one full-width bar along the bottom: body, policy link, Accept, Reject and Select. Reject keeps the same size and weight as Accept. Select opens the card for the category view.
Sharing one decision across subdomains
By default the decision is stored in localStorage, which is scoped to the origin — so
www.example.com and lp.example.com never share it, and a visitor moving between them is
asked twice. When one brand spans several hosts, set storage.cookieDomain and the record
moves to a cookie scoped to that domain:
export const consentConfig: BrandConfig = {
brand: "example",
locale: "en-GB",
privacyPolicyUrl: "https://www.example.com/privacy-policy",
storage: { cookieDomain: ".example.com" }, // leading dot optional
copy: copyForLocale("en-GB"),
};Cookie attributes: Path=/, SameSite=Lax, Max-Age from storage.cookieMaxAgeDays
(default 180), and Secure on https (omitted on http so localhost still works). A cookie is
the only mechanism browsers offer here — the old iframe + postMessage trick is broken by
Safari ITP and Chrome's storage partitioning.
Two things to know:
cookieDomainmust be a domain the page can set a cookie for — a parent of the current host, and not a public suffix. Browsers silently drop cookies that violate this, so a wrong value degrades to "no consent is ever remembered" rather than to an error. Verify in DevTools → Application → Cookies after the first deploy.- Switching an existing site to a cookie re-prompts its current visitors once. There is deliberately no localStorage→cookie migration: a stale per-origin record would defeat the point of sharing. Every property on the shared domain should be switched together.
The consent record is itself strictly necessary, so storing it needs no consent.
Locale presets
copy can come from a bundled preset instead of being written out per site. Presets ship for
en-GB and sv-SE. They are deliberately generic — plain, voiceless boilerplate meant as a
lawful starting point, not finished copy. Expect to override them for your own voice:
import { copyForLocale, SV_SE_COPY, type BrandConfig } from "@bulut5/ck";
export const consentConfig: BrandConfig = {
brand: "example",
locale: "sv-SE",
privacyPolicyUrl: "https://example.com/integritetspolicy/",
copy: SV_SE_COPY, // or: copyForLocale("sv-SE")
};copyForLocale falls back to the bare language (sv-FI → sv-SE) and then to en-GB, so an
unknown tag still renders a banner. Override wholesale, or spread over a preset to change only
some strings: copy: { ...SV_SE_COPY, body: "…your wording…" }.
Whatever you override, keep reject worded exactly as plainly as accept — ICO/PECR and IMY both require equal prominence.
The host page should set the Google Consent Mode default to denied inline in <head> before
the bundle loads; the banner flips signals to granted on consent. Tailwind consumers should add
./node_modules/@bulut5/ck/dist/**/*.{js,mjs} to their content so the banner classes compile.
MIT licensed.
