Skip to main content
Cookie consent is the work every public website needs and almost every indie postpones until a buyer’s legal team flags it. vibestrap ships a working banner so you don’t have to choose between “shipping fast” and “compliant on day one”:
  • 🍪 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.
Architecture is intentionally split into three layers:

Prerequisites

  • Nothing to install — vanilla-cookieconsent is already a dependency.
  • siteConfig.cookieConsent.enable defaults to true (the banner ships on).

Step-by-step

1. Confirm it’s enabled in src/config/site.ts

2. Verify the analytics gating

In src/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, bump siteConfig.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

In dev mode, the banner is intentionally skipped so you don’t see it on every reload (see process.env.NODE_ENV check in cookie-consent.tsx). To see the live banner:
You should see:
  • 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. The default expiry is 395 days (~13 months), matching:
  • Stripe (13 months)
  • Microsoft (13 months)
  • Cloudflare (13 months)
This is the upper bound recommended by the UK ICO and France’s CNIL — long enough that returning users aren’t bannered repeatedly, short enough that consent stays demonstrably fresh. Sites going for shorter windows (Vercel / Linear / Notion at 6 months) are also compliant; we chose 13 to match the most rigorous reference implementations. To adjust, edit siteConfig.cookieConsent.expiresAfterDays.

Customizing the banner copy

All banner text lives in messages/{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:
For a complete UI override, set 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 by src/components/cookie-consent/cookie-preferences-link.tsx and is automatically wired — don’t remove it.

Common pitfalls

  1. 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 start to see the real banner.
  2. 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.revision in the same PR that adds a tracker.
  3. Analytics still loading after Decline. Check that the script render in src/analytics/analytics.tsx is actually behind allowAnalytics — 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.
  4. Cookie banner shows broken HTML. If you customized the banner footer string (links to Privacy Policy / Privacy Choices), make sure the HTML is valid — the lib renders it raw.
  5. Locale mismatch. The banner reads from <html lang="…"> (set by next-intl). If your custom layout sets a different lang attribute, 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. If you’re cookie-free (only running Vercel Analytics / Plausible / Umami) and want to skip the banner, set:
This still mounts the React Context (so useConsent() keeps working), but treats every user as having consented to everything. Skip the banner UI entirely.

Official resources