- 🍪 Opt-in by default — analytics scripts (Google Analytics, Microsoft Clarity, PostHog) wait for the user’s “Accept” before loading. No data collected before consent.
- ⚖️ Globally-framed compliance — covers GDPR (EU/UK), CCPA / CPRA (California), LGPD (Brazil), PIPL (China) under one “Your Privacy Choices” mechanism; opt-out is one click from the banner or footer.
- 🧹 Cleanup on revoke — when a user toggles analytics off after accepting, we erase the cookies that were set. GDPR Article 7(3).
- 🌍 Bilingual — en / zh follow vibestrap’s existing i18n, no extra language detection.
- 🎨 Two-button banner — Accept / Reject only, no third “Customize” button. Granular per-category control lives behind the footer “Cookie Preferences” link (Hick’s law: fewer choices = faster decision).
- 🛠️ Built on
vanilla-cookieconsent(9k+ stars, MIT). Easy to retheme via CSS variables.
Prerequisites
- Nothing to install —
vanilla-cookieconsentis already a dependency. siteConfig.cookieConsent.enabledefaults totrue(the banner ships on).
Step-by-step
1. Confirm it’s enabled in src/config/site.ts
2. Verify the analytics gating
Insrc/analytics/analytics.tsx you’ll see scripts split into two buckets:
allowAnalytics is true only after the user clicks Accept (or already
consented in a previous visit, within the 12-month expiry window).
3. Bump revision when cookies change
If you add a new analytics provider or switch one out, bumpsiteConfig.cookieConsent.revision by one. Every previously-consented
user will see the banner again so they can re-consent under the new
policy. This is GDPR best practice.
4. Verify in the browser
process.env.NODE_ENV check in
cookie-consent.tsx). To see the live banner:
- A bottom-right toast on first visit
- Two buttons: Accept all / Reject all
- A footer with: Privacy Policy · Cookie Policy · Your Privacy Choices
User journey — every scenario, aligned to industry leaders
The following matrix maps every common user flow to vibestrap’s behavior and the corresponding behavior at Stripe / Microsoft / Vercel / Cloudflare / Linear / OpenAI. The implementation matches the industry norm everywhere — no proprietary or surprising behavior.Cookie expiry rationale (13 months)
The default expiry is 395 days (~13 months), matching:- Stripe (13 months)
- Microsoft (13 months)
- Cloudflare (13 months)
siteConfig.cookieConsent.expiresAfterDays.
Customizing the banner copy
All banner text lives inmessages/{en,zh}.json under the
CookieConsent namespace. To change the title, button labels, or
descriptions, edit those keys — both files in lockstep (the i18n audit
catches drift).
Customizing the banner UI
vanilla-cookieconsent exposes ~30 CSS variables for colors, radii,
shadows, and font sizes. Override them by editing the imported CSS or
adding a custom CSS layer:
disablePageInteraction: false and
write your own Tailwind-styled container — but the default already
matches Linear / Stripe-style toasts.
Withdrawal entry point
GDPR Article 7(3) requires that withdrawing consent be as easy as giving it. vibestrap renders a Cookie Preferences link in the footer’s Legal section that re-opens the modal. This is mounted bysrc/components/cookie-consent/cookie-preferences-link.tsx and is
automatically wired — don’t remove it.
Common pitfalls
-
Banner doesn’t show in dev. This is intentional — dev mode
short-circuits to “everything accepted” so you can test analytics
without bannering yourself on every reload. Use
pnpm build && pnpm startto see the real banner. -
Forgot to bump revision after adding a new tracker. Existing
users won’t be re-prompted, and they’ll be tracked under their old
consent (which didn’t cover the new tracker). Always bump
siteConfig.cookieConsent.revisionin the same PR that adds a tracker. -
Analytics still loading after Decline. Check that the script
render in
src/analytics/analytics.tsxis actually behindallowAnalytics— easy to add a new tracker and forget the gate. The convention: anything that drops a cookie is gated, anything cookie-free (Vercel / Plausible / Umami) renders unconditionally. -
Cookie banner shows broken HTML. If you customized the banner
footerstring (links to Privacy Policy / Privacy Choices), make sure the HTML is valid — the lib renders it raw. -
Locale mismatch. The banner reads from
<html lang="…">(set by next-intl). If your custom layout sets a differentlangattribute, the banner falls back to English.
”Your Privacy Choices” — global framing
The banner footer links to/privacy#privacy-choices, a globally-framed
section in the privacy policy that covers opt-out rights for everyone:
GDPR (EU / UK), CCPA / CPRA (California), LGPD (Brazil), PIPL (China).
The opt-out mechanism is identical for every visitor (banner,
footer link, or email); the section then names each region’s specific
right (e.g. California’s legally-named “Do Not Sell My Personal
Information”) so a regulator searching for the phrase still finds it.
This mirrors how Apple, Microsoft, and Stripe have moved away from
California-only “Do Not Sell” headers toward a global “Your Privacy
Choices” frame with named sub-rights. If you target California heavily
and want a dedicated form for opt-out requests, add a
/privacy/privacy-choices page; the inline section is the indie-stage
default.
Disabling cookie consent entirely
If you’re cookie-free (only running Vercel Analytics / Plausible / Umami) and want to skip the banner, set:useConsent() keeps working),
but treats every user as having consented to everything. Skip the
banner UI entirely.
Official resources
- vanilla-cookieconsent docs: cookieconsent.orestbida.com
- GDPR Article 7 (consent): gdpr-info.eu/art-7-gdpr
- CCPA / CPRA: oag.ca.gov/privacy/ccpa