@caprail-dev/consent
v0.1.4
Published
Region-aware, analytics-agnostic cookie-consent banner and state for React — Shadcn-styled, Tailwind-themeable.
Readme
@caprail-dev/consent
A region-aware cookie-consent banner and consent-state layer for React, made
to pair with @caprail-dev/analytics
— but analytics-agnostic, so it gates any tracker (GA, PostHog, your own).
- Consent matrix as state —
useConsent()gives any component the live per-category grants and the actions to change them. - Tailwind, zero theme setup — styled with Tailwind's default palette
(zinc), so it looks polished out of the box in any Tailwind project — no Shadcn
tokens required. Override any color with
className/classNames. - Region-aware — pass the visitor's country and it switches between opt-in (EEA/UK/CH/BR), opt-out (US-CA, with a "Do Not Sell/Share" link), and notice.
- Mobile-first — bottom-anchored, stacked buttons that go inline on
sm:, safe-area inset for notch devices, reduced-motion aware, focus-trapped dialog. - Extendable — register third-party services with a
Loaderthat mounts only while its category is granted; nothing is bundled.
Install
bun add @caprail-dev/consent
# peers (you already have these in a React app)
bun add react react-domSetup (one required step)
Let Tailwind scan the package. The components are styled with Tailwind's
default palette, so there are no tokens to define — but Tailwind v4 does not scan
node_modules by default, so it won't emit those utility classes unless you
point it at the package. Add this to the CSS file where you import Tailwind:
@import "tailwindcss";
@source "../node_modules/@caprail-dev/consent/dist";(Adjust the relative path to your CSS file's location.)
Usage
import { ConsentProvider } from "@caprail-dev/consent";
export default function RootLayout({ children }) {
return (
<ConsentProvider /* country="DE" region="BY" */>{children}</ConsentProvider>
);
}The provider renders the banner and the consent-gated services itself — one tag.
Region detection
The package never fetches geo. Pass the visitor's country (and subdivision for US-CA) from your own edge — e.g. in a Next.js layout from the request headers:
import { headers } from "next/headers";
const h = await headers();
<ConsentProvider
country={h.get("x-vercel-ip-country")}
region={h.get("x-vercel-ip-country-region")}
>With no country the mode is notice and the banner is informational.
Reading and reacting to consent
"use client";
import { useConsent } from "@caprail-dev/consent";
function Analytics() {
const { consent } = useConsent();
if (!consent.analytics) return null;
return /* … load analytics … */;
}Non-React listeners can use the caprail:consent window event, dispatched on
every settled change:
window.addEventListener("caprail:consent", (e) => {
const { analytics, marketing } = (e as CustomEvent).detail;
});Gating third-party services
import Script from "next/script";
import { ConsentProvider, type ServiceDefinition } from "@caprail-dev/consent";
const services: ServiceDefinition[] = [
{
id: "ga",
name: "Google Analytics",
category: "analytics",
Loader: ({ cookieless }) => (
<Script src="https://www.googletagmanager.com/gtag/js?id=G-XXXX" />
),
},
];
<ConsentProvider services={services}>{children}</ConsentProvider>;A service's Loader mounts only while its category is granted and unmounts on
revoke. cookieless (from the provider's cookieless prop) lets a loader run
its SDK in a memory-only mode.
Custom categories
<ConsentProvider
categories={[
{ id: "necessary", label: "Necessary", description: "Always on." },
{ id: "analytics", label: "Analytics", description: "Usage insights." },
{ id: "ads", label: "Advertising", description: "Personalized ads." },
]}
/>necessary is always present and locked. ConsentState is keyed by your ids.
ConsentProvider props
| Prop | Type | Default | Notes |
| ------------- | --------------------- | ----------------------------- | ----------------------------------------------- |
| country | string \| null | null | ISO-3166-1 alpha-2, e.g. x-vercel-ip-country. |
| region | string \| null | null | Subdivision, used for US-CA. |
| categories | CategoryConfig[] | necessary/analytics/marketing | Toggles shown in "Manage". |
| services | ServiceDefinition[] | [] | Trackers to gate + list. |
| forceBanner | boolean | false | Show even with no non-essential category. |
| cookieless | boolean | false | Forwarded to each Loader. |
| cookieName | string | "caprail_consent" | First-party cookie name. |
| onChange | (state) => void | — | Called after every settled change. |
useConsent()
Returns { consent, mode, region, categories, categoriesInUse, showBanner,
acceptAll, rejectAll, setCategory, save, openBanner }. Call openBanner() from a
footer "Cookie settings" link to let visitors revise a decision.
License
MIT
