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:
LayerFileWhat it does
Libraryvanilla-cookieconsentDOM injection, GDPR / CCPA logic, persistence
Wrappersrc/components/cookie-consent/cookie-consent.tsxReact Provider, language hookup, lifecycle
ConsumeruseConsent() in src/analytics/analytics.tsxGates the actual tracking scripts

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

cookieConsent: {
  enable: true,        // mounts the banner
  revision: 1,         // bump this when cookies change to force re-consent
  expiresAfterDays: 365,
},

2. Verify the analytics gating

In src/analytics/analytics.tsx you’ll see scripts split into two buckets:
{/* No consent needed — cookie-free providers */}
{siteConfig.analytics.vercel && <VercelAnalytics />}
{siteConfig.analytics.plausible && <PlausibleScript />}
{siteConfig.analytics.umami && <UmamiScript />}

{/* Consent-gated — drops cookies */}
{siteConfig.analytics.googleAnalytics && allowAnalytics && <GoogleAnalytics />}
{siteConfig.analytics.posthog && allowAnalytics && <PostHogScript />}
{siteConfig.analytics.clarity && allowAnalytics && <ClarityScript />}
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

pnpm dev
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:
pnpm build && pnpm start
# or visit production
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.
Scenariovibestrap behaviorIndustry standard
First-ever visitBanner appears bottom-rightSame (Stripe / Vercel / Linear / OpenAI)
Visit after Accept (within 13 months)No banner, GA + Clarity loadSame
Visit after Reject (within 13 months)No banner, GA + Clarity stay disabledSame
Visit after consent expires (>13 months)Banner reappears (treated as new visit)Same — Stripe/MS use 13mo, others 6-12mo
Refresh / navigate without choosingBanner stays visible until user picksSame
Click X / close icon without choosingTreated as “Reject all” — necessary onlySame — 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 dataBanner reappears (no cookie present)Same
Incognito / private windowBanner appears every sessionSame
Cross-deviceEach device decides independentlySame — 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 bannerNavigate to /privacy; banner stays visibleSame — must not lose consent state mid-read
Click Cookie Preferences in footerRe-opens preferences modalSame — required by GDPR Article 7(3)
Toggle Analytics off after accepting_ga / _clck / MUID deleted on next page loadSame — required by GDPR Article 7(3)
Toggle Analytics on after decliningGA + Clarity load on next renderSame
Reject vs site functionalityAll site features keep working (login, payments, newsletter)Same — GDPR forbids cookie-walling
Affiliate ?ref=xxx referral with marketing rejectedCookie not stored (marketing category off)Same — Stripe / Vercel apply same gating
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:
:root {
  --cc-bg: #fff;
  --cc-primary-color: var(--accent-tech);  /* match vibestrap brand */
  --cc-border-radius: 0.5rem;
  /* see: https://cookieconsent.orestbida.com/advanced/customization.html */
}
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:
// src/config/site.ts
cookieConsent: { enable: false, revision: 1, expiresAfterDays: 365 },
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