@meetreeve/consent-kit
v0.1.2
Published
Cookie-consent banner for Reeve host apps — vanilla-cookieconsent engine wrapped in a themeable React primitive, with GDPR/CCPA presets and Reeve pixel/GA4/Meta/PostHog autoBlock wiring.
Keywords
Readme
@meetreeve/consent-kit
Cookie-consent banner for Reeve host apps — a themeable React primitive wrapping vanilla-cookieconsent (MIT, orestbida), with GDPR/CCPA presets and Reeve's pixel/GA4/Meta/PostHog
autoBlockwiring.
Ships one component: <ConsentBanner>. It renders in the host page's own DOM (not an iframe — see packages/embed/src/surfaces/, which is a deliberately different pattern for remote-app embeds) because a cookie banner has to gate <script> tags that live on the parent page; an iframe has no authority over that.
Scope
This package builds the popup + engine + theming + autoBlock config. It does not:
- Call the consent ledger API.
onConsentis a pure callback — the browser can't hold the HMAC secret the ledger endpoint requires. Wiring a BFF proxy toPOST /api/compliance/v1/consentis DEV-4356. - Detect geography (GDPR vs CCPA).
regionis caller-supplied. Deriving it fromx-vercel-ip-countryis also DEV-4356. - Mount itself into reeve-frontend or gate
tracking-slots.tsx. Also DEV-4356. - Seed the
legal_documentsdoc_type="cookie"row the ledger needs to validate against — prod currently has zero rows of that type; that's a content/ops prerequisite tracked on DEV-4356, not this package.
Install
pnpm add @meetreeve/consent-kitreact / react-dom (^18 || ^19) are peer dependencies. vanilla-cookieconsent and @meetreeve/embed are regular dependencies (installed automatically).
Quick start
import { ConsentBanner, toLedgerPayload } from '@meetreeve/consent-kit';
import '@meetreeve/consent-kit/styles/cookieconsent.css';
export function CookieConsentGate({ brand }: { brand: HostAppBrand }) {
return (
<ConsentBanner
hostApp="acme-tenant"
brand={brand}
region="gdpr" // or "ccpa" / "none" — geo-resolution is the caller's job (DEV-4356)
onConsent={(categories, action) => {
// Pure callback — no network call happens here. DEV-4356's BFF
// proxy takes this and POSTs it to the consent ledger.
const payload = toLedgerPayload(categories, action);
// fetch('/api/consent', { method: 'POST', body: JSON.stringify(payload) })
}}
/>
);
}region="none" renders nothing and never touches the underlying engine — useful while a host app hasn't wired geo detection yet.
Category model
| Category | Always on? | Gates (via tracking-slots.tsx, DEV-3322) |
|---|---|---|
| necessary | Yes — readOnly, not user-togglable | n/a |
| analytics | No | PostHog |
| marketing | No | Meta Pixel, GA4, Reeve's first-party pixel loader (adbot/static/pixel/r.js) |
The full service→category map lives in REEVE_AUTOBLOCK_SERVICES (exported from the package root) — treat the pixel-loader bucketing as a reviewable default, not dogma.
GDPR vs CCPA presets
| | region="gdpr" | region="ccpa" |
|---|---|---|
| Mode | opt-in | opt-out |
| Default state | analytics/marketing start rejected | analytics/marketing start accepted |
| Primary framing | "We use cookies" / Accept all / Reject all | "Your privacy choices" / Do Not Sell or Share My Personal Information |
| Visitor action | Must affirmatively opt in | May opt out ("Do Not Sell") |
Both presets share the same category model and autoBlock wiring — only mode, default category.enabled, and modal copy differ. See src/presets/gdpr.ts / src/presets/ccpa.ts.
autoBlock (script-tag gating)
vanilla-cookieconsent v3 implements what v2 called "autoBlock" via manageScriptTags (on by default in this package's engine config) plus a data-category/data-service convention. Whoever injects a tracking script (DEV-4356's mount, today) tags it:
<script type="text/plain" data-category="marketing" data-service="ga4">
// GA4 bootstrap
</script>Until the visitor accepts the marketing category (and, if declared, the ga4 service specifically), the script stays inert (type="text/plain" isn't a runnable MIME type). The moment consent is granted, vanilla-cookieconsent clones the tag without the blocking type attribute and swaps it into the DOM, making it execute. This is a document-wide scan — the tagged <script> doesn't need to live inside <ConsentBanner>'s own container.
Theming contract
<ConsentBanner brand={brand} /> re-skins the underlying engine by writing --cc-* CSS custom properties (vanilla-cookieconsent's own variable contract — see its dist/css-components/*.css) as inline styles on the banner's container element, so they cascade only to that instance.
brandToConsentVars(brand: HostAppBrand) (also exported standalone) does the mapping. It is a separate function from @meetreeve/embed/brand's brandToCssVars() — that helper targets Documenso's CssVars naming (--primary, --accent, …); vanilla-cookieconsent has its own prefixed names, so this package maps HostAppBrand directly rather than piping through brandToCssVars's output.
| HostAppBrand field | --cc-* variable |
|---|---|
| primary_color | --cc-btn-primary-bg, --cc-btn-primary-border-color, --cc-toggle-on-bg |
| primary_foreground | --cc-btn-primary-color |
| accent_color | --cc-btn-primary-hover-bg, --cc-link-color (falls back to primary_color) |
| background_color | --cc-bg (omitted if null) |
| muted_color | --cc-btn-secondary-bg, --cc-cookie-category-block-bg (omitted if null) |
| muted_foreground | --cc-btn-secondary-color, --cc-secondary-color (omitted if null) |
| border_color | --cc-separator-border-color (omitted if null) |
| radius | --cc-modal-border-radius, --cc-btn-border-radius, --cc-pm-toggle-border-radius |
| font_family_sans | --cc-font-family (omitted if null) |
| extra_css_vars | Any key already prefixed --cc- wins over the mapped defaults (same escape-hatch precedence as brandToCssVars) |
You must still import the base stylesheet once (it supplies layout, spacing, and the light/dark scheme defaults the --cc-* variables plug into):
import '@meetreeve/consent-kit/styles/cookieconsent.css';This is vanilla-cookieconsent's own CSS, copied into this package's build output — no CDN fetch, no external font @import. Self-contained.
onConsent → ledger payload contract
onConsent(categories, action) fires whenever the visitor's consent state is (re)confirmed (initial accept/reject, or a saved preferences-panel change). toLedgerPayload() shapes that into what DEV-4356's BFF proxy should POST to the DEV-2614 consent ledger (/api/compliance/v1/consent, reeve-services):
interface ConsentLedgerPayload {
doc_type: 'cookie'; // already a first-class value — api/models/compliance.py:37
subject_type: 'visitor'; // anonymous visitor, not an authenticated user/tenant
action: 'all' | 'custom' | 'necessary';
categories: ('necessary' | 'analytics' | 'marketing')[];
source: 'cookie_banner';
}action mirrors vanilla-cookieconsent's own AcceptType vocabulary (all / custom / necessary) rather than inventing a parallel one:
'all'— visitor clicked Accept all.'necessary'— visitor clicked Reject all / Do Not Sell (only the always-on category applies).'custom'— visitor saved a specific subset via "Manage preferences".
This package never constructs the HTTP request — the ledger write is HMAC-signed server-to-server, and a browser can't hold that secret.
API
<ConsentBanner> (@meetreeve/consent-kit/primitives, also re-exported from the root)
| Prop | Type | Notes |
|---|---|---|
| hostApp | string | Debug/e2e marker only (data-consent-kit-host). Never transmitted. |
| brand | HostAppBrand | From @meetreeve/embed/brand. Re-skins via --cc-* vars. |
| region | 'gdpr' \| 'ccpa' \| 'none' | 'none' renders nothing. |
| onConsent? | (categories: ConsentCategory[], action: ConsentAction) => void | Pure callback; safe to pass a new inline function each render — identity changes don't remount the engine. |
| policyLinks? | { cookiePolicyHref?: string; privacyPolicyHref?: string } | Caller-supplied links to the host app's own policy documents (DEV-7210), rendered as plain anchors in the consent modal's footer in every region. Caller-supplied because host apps live on different domains. Omit for no footer at all. |
| layout? | CookieConsent.ConsentModalLayout | Overrides guiOptions.consentModal.layout (DEV-9938). Omit to use the region's own default — see "Narrow-viewport GDPR default" below. |
| position? | CookieConsent.ConsentModalPosition | Overrides guiOptions.consentModal.position (DEV-9938). Same override relationship as layout. |
| className? | string | Applied to the container <div>. |
Narrow-viewport GDPR default (DEV-9938)
gdprPreset ships layout: 'box', position: 'bottom right' — a floating
card anchored to the bottom-right corner. At narrow viewports (at or under a
640px width, checked once at mount via window.matchMedia) that card
measured tall enough to occlude a template's hero heading/subhead, so
region="gdpr" auto-switches to a shorter, full-width bottom bar instead
unless the caller passes an explicit layout and/or position — an
explicit prop always wins, at any viewport width. region="ccpa" is
unaffected; its preset already ships a bar layout.
This default is a bundle of three things, all gated on the same region/width/no-explicit-prop condition:
layout: 'bar', position: 'bottom'instead of'box'/'bottom right'.- A shorter
consentModal.description(same substance, fewer words — "Manage preferences" still shows the full sentence). - A
max-height: 230pxcap on the modal itself (with internal scroll), shipped as part of@meetreeve/consent-kit/styles/cookieconsent.css— no extra import needed.
At >640px (or with an explicit layout/position prop), none of this
applies — the banner renders exactly as it did before this default
existed.
Presets (@meetreeve/consent-kit/presets)
gdprPreset, ccpaPreset — ConsentPreset objects (mode, guiOptions, translation) consumed internally by <ConsentBanner>. Exported for callers building a custom engine config.
Other root exports
toLedgerPayload(categories, action) => ConsentLedgerPayload— pure data shaper, see above.REEVE_AUTOBLOCK_SERVICES— the service → category map used to populate each category'sserviceslist in the preferences panel.brandToConsentVars(brand) => Record<string, string>— the theming mapping, exposed standalone for advanced integrations.
Deviations from the vendored engine's defaults
hideFromBots: false(library default:true). The library's bot heuristic checksnavigator.webdriver/ a UA regex and silently skips rendering when it thinks it's a crawler — which makes browser-automation test/CI environments non-deterministic. Reeve already gates search-engine indexing elsewhere; disabling this here trades a marginal SEO nicety for predictable behavior everywhere the banner runs.onConsent/onChangededuping: the vendored engine can fire both its ownonConsentandonChangehooks for a single user action inopt-outmode. This package's<ConsentBanner onConsent>dedupes by comparing the resulting(categories, action)tuple against the last emission, so callers always get exactly one call per distinct outcome.
License
This package: UNLICENSED (internal, mirrors @meetreeve/chat-kit/@meetreeve/embed).
Vendored dependency vanilla-cookieconsent (orestbida): MIT. See node_modules/vanilla-cookieconsent/LICENSE after install, or upstream.
