- 🍪 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.
| Layer | File | What it does |
|---|---|---|
| Library | vanilla-cookieconsent | DOM injection, GDPR / CCPA logic, persistence |
| Wrapper | src/components/cookie-consent/cookie-consent.tsx | React Provider, language hookup, lifecycle |
| Consumer | useConsent() in src/analytics/analytics.tsx | Gates the actual tracking scripts |
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.| Scenario | vibestrap behavior | Industry standard |
|---|---|---|
| First-ever visit | Banner appears bottom-right | Same (Stripe / Vercel / Linear / OpenAI) |
| Visit after Accept (within 13 months) | No banner, GA + Clarity load | Same |
| Visit after Reject (within 13 months) | No banner, GA + Clarity stay disabled | Same |
| Visit after consent expires (>13 months) | Banner reappears (treated as new visit) | Same — Stripe/MS use 13mo, others 6-12mo |
| Refresh / navigate without choosing | Banner stays visible until user picks | Same |
| Click X / close icon without choosing | Treated as “Reject all” — necessary only | Same — GDPR forbids treating it as Accept |
| Cookie revision bumped (policy change) | Banner reappears (forced re-consent) | Same — Stripe/MS use version field too |
| Cleared browser data | Banner reappears (no cookie present) | Same |
| Incognito / private window | Banner appears every session | Same |
| Cross-device | Each device decides independently | Same — consent is per-browser-storage |
| Browser sends Sec-GPC: 1 (Brave / DuckDuckGo / Firefox+ext) | No banner; auto-Reject (only necessary) | Same — Microsoft + 2024+ US state laws require this |
| Click Privacy Policy in banner | Navigate to /privacy; banner stays visible | Same — must not lose consent state mid-read |
| Click Cookie Preferences in footer | Re-opens preferences modal | Same — required by GDPR Article 7(3) |
| Toggle Analytics off after accepting | _ga / _clck / MUID deleted on next page load | Same — required by GDPR Article 7(3) |
| Toggle Analytics on after declining | GA + Clarity load on next render | Same |
| Reject vs site functionality | All site features keep working (login, payments, newsletter) | Same — GDPR forbids cookie-walling |
Affiliate ?ref=xxx referral with marketing rejected | Cookie not stored (marketing category off) | Same — Stripe / Vercel apply same gating |
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