vTilt
By roleFoundersKnow what to fix firstMarketersSee what happens after the clickCMOsSee what your budget really buys
By businessSaaSTurn signups into active usersEcommerceSee why shoppers abandon cartsAgenciesProve your work to clientsEnterpriseOne customer record for every team
MeasureWeb AnalyticsFind where buyers leaveSession ReplaySee why they left, not just that they didAdsKnow what paid visitors do nextIntegrationsOne snippet feeds your stack
ThinkPeopleOne profile per customerCustomer MemoryvTilt remembers every visitorSite KnowledgeAnswers and audits from your own pages
ActAsk AIAnswers from real behaviorAI ChatChat that knows the visitorEmail CampaignsEmail from real visits
PricingWhy vTiltDocs
Docs / Cookie consent
Getting Started
OverviewInstallInitializeIdentify usersTrack eventsLogout & resetVerify eventsCommon mistakes
Guides
Event forwardingReverse proxyRealtime dashboardSite knowledgeStudioWebsite chat MCP
Frontend frameworks
Next.jsNuxt.jsVue.jsReactReact RouterRemixGatsbySvelte / SvelteKitAstroAngularTanStack StartDocusaurus
Backend frameworks
NestJSHonoCloudflare WorkersDjangoFlaskLaravelPhoenixRuby on Rails
Backend languages
PythonPHPRubyElixirGoJava.NET / C#Rust
Stack guides
Vue + PHP
API Reference
Browser SDK
Script bundlesAutocaptureWeb VitalsSession recordingChat widgetOutbound messagesCookie consentFeature readinessRemote configurationDebug logging
Node SDK
Install & setupCapture, identify & aliasContext & shutdownGlobal properties & opt-outError tracking
MCP server
Guides
OverviewAuthenticationOAuthAgent skills (prompts)AI intelligenceSite knowledgeGoogle AdsMeta Ads
Client setup
CursorClaude DesktopVS CodeChatGPTCodex

Getting Started

OverviewInstallInitializeIdentify usersTrack eventsLogout & resetVerify eventsCommon mistakes

Guides

Event forwardingReverse proxyRealtime dashboardSite knowledgeStudioWebsite chat MCP

API Reference

Script bundlesAutocaptureWeb VitalsSession recordingChat widgetOutbound messagesCookie consentFeature readinessRemote configurationDebug logging

MCP server

On this page

On this page

  • Enable
  • What it gates
  • Styling
  • Opt-out regions get a link, not a banner
  • Light and dark
  • Shape
  • Wording
  • Letting visitors change their mind
  • Forcing a regime
  • What happens before a visitor answers
  • Running with no banner at all
  • Global Privacy Control
  • Configuring from the dashboard
  • Driving consent from your own CMP
  • Reading the resolved region
DocsAPI ReferenceBrowser SDKCookie consent

Cookie consent

Last updated October 13, 2026

A region-aware consent banner that only appears where the law requires one. Themable, under 4 KB, and lazy-loaded so it costs nothing when it isn't needed.

vTilt collects no personal data by default, so basic analytics needs no consent banner. But the moment you add advertising tags, Meta pixel cookies, or session replay, some of your visitors do need one — and only some of them.

The consent banner resolves each visitor's privacy regime on the server and shows the least intrusive thing that satisfies it:

Visitor inRegimeWhat they see
EU/EEA, UKopt-inA bar with Accept all, Reject all, and Preferences. Non-essential tracking is withheld until they answer.
California and other US states with privacy laws; Switzerland; Brazilopt-outNo banner. A persistent Do not sell or share my personal information link after your page content. Tracking is not interrupted.
Everywhere elsenoneNothing. The banner code is never even downloaded.

#Enable

vt.init('YOUR_PROJECT_TOKEN', {
  api_host: 'https://www.vtilt.com',
  consent_banner: { enabled: true },
})
typescript

That is the whole setup. The regime is resolved per visitor, so you do not maintain a country list.

You can also turn it on without touching your snippet, under Project settings → Tracking → Privacy — see Configuring from the dashboard below.

Note

The banner is lazy-loaded and costs nothing until it is needed. It is not part of array.js — it lives in consent-banner.js (under 4 KB gzipped), fetched only when a visitor's region actually calls for a banner. Visitors in unregulated regions, and returning visitors who already chose, download none of it.

Note

If you also set require_consent: true, every visitor is asked regardless of where they are — the setting is a floor, and the region cannot lower it. Without the banner enabled, require_consent: true still means nothing is captured until you call vt.setConsent() yourself.

#What it gates

A choice is written to a first-party cookie (_vtilt_consent, one year) and drives three things automatically:

  • vTilt capture — in opt-in regions, non-essential events are withheld until the visitor accepts. Identity events ($identify, $alias, $set) are always allowed.
  • Google Consent Mode v2 — if you use the Google tag integration, ad_storage, ad_user_data, ad_personalization, and analytics_storage are set from the visitor's choice.
  • Meta cookies — _fbp / _fbc are only written when advertising consent is granted.

The visitor's choice is also attached to every event as $consent_analytics, $consent_marketing, and $consent_advertising, so you can see in the dashboard what was permitted.

#Styling

Set any of these and the rest keep their defaults. All of them can also be set in the dashboard, with a live preview — see Configuring from the dashboard.

vt.init('YOUR_PROJECT_TOKEN', {
  api_host: 'https://www.vtilt.com',
  consent_banner: {
    enabled: true,
    position: 'bottom',
    privacyPolicyUrl: 'https://example.com/privacy',
    theme: {
      appearance: 'auto',
      primaryColor: '#111827',
      primaryTextColor: '#ffffff',
      backgroundColor: '#ffffff',
      textColor: '#0f172a',
      mutedTextColor: '#64748b',
      borderColor: '#e2e8f0',
      borderRadius: '12px',
      fontFamily: 'Inter, system-ui, sans-serif',
      overlay: false,
    },
  },
})
typescript

overlay: true dims the page behind the dialog in opt-in regions. It has no effect in opt-out regions, which never raise a dialog unprompted.

#Opt-out regions get a link, not a banner

The CCPA does not require a consent banner. It requires a Notice at Collection and — if you sell or share personal information — a clear link that opens the opt-out. So in opt-out regions vTilt shows no banner at all and instead appends a single line to the end of your page:

Do not sell or share my personal information

It sits in your page's flow, after your content, the same place you would put it in a footer. It never covers anything, never needs dismissing, and stays put after a visitor makes a choice so they can change it later. Selecting it opens the full preferences dialog.

Style it from your own CSS through its shadow parts:

#vtilt-privacy-link-host::part(privacy-link) {
  font-size: 12px;
  opacity: 1;
}
#vtilt-privacy-link-host::part(privacy-link-wrap) {
  justify-content: flex-start;
  padding: 8px 24px;
}
css

Rename it with the noticeOptOut copy key if you prefer "Your privacy choices" — the default is the wording the statute names.

Note

because vTilt honours Global Privacy Control, a visitor who sends that signal is already opted out before the link is ever used. California lets a business honour opt-out preference signals in lieu of posting the link, so if you do not sell or share data you have both routes covered.

borderRadius applies to the buttons. The bar itself is square, because it runs edge to edge (see below).

#Light and dark

appearance picks the palette:

ValueBehaviour
auto (default)Follows the visitor's device setting, via prefers-color-scheme
lightAlways light
darkAlways dark

Any colour you set wins in both palettes. Theme colours are applied as inline custom properties on the banner's host element, which outranks the built-in palettes — so backgroundColor: '#ffffff' stays white even under appearance: 'dark'. Set colours only when you want them fixed; leave them unset to let auto do its job.

Tip

if your site has its own dark mode that does not follow the device setting — a toggle in your header, say — leave the banner's colours unset and set appearance yourself when you switch themes, by calling vt.init with the value you want or restyling ::part(panel).

The category list uses real checkboxes, so the banner also sets color-scheme with each palette; without it the browser would paint a white checkbox on the dark panel.

#Shape

Both variants render as a full-width bar pinned to the bottom edge (or the top, with position: 'top') — not a centred, floating card. It carries a hairline border on the edge facing your page and no corner radius, so it reads as part of the browser rather than something dropped over your content.

The bar spans the viewport, but its contents do not: text and buttons sit in a column capped at 1180px and centred, so a sentence is never set across an entire widescreen monitor. Text goes left, buttons right, on one row. Below 640px it stacks, with the buttons sharing a row underneath the text.

Choosing Preferences expands the bar: the category list appears above the buttons and Save choices takes the place of the Preferences button, so every way of answering stays in one row.

To widen or narrow the readable column, restyle content:

#vtilt-consent-host::part(content) {
  max-width: 960px;
}
css

The banner renders in a Shadow DOM root, so your page's CSS cannot break it and its CSS cannot leak out. For control beyond the theme options, its elements expose part names:

#vtilt-consent-host::part(panel) {
  box-shadow: none;
  border-top-width: 2px;
}
#vtilt-consent-host::part(accept) {
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
css

Available parts: panel, content, scrim, title, description, actions, accept, reject, preferences, save, categories, opt-out, dismiss, privacy-link.

#Wording

Every string can be replaced — pass only the ones you want to change:

vt.init('YOUR_PROJECT_TOKEN', {
  api_host: 'https://www.vtilt.com',
  consent_banner: {
    enabled: true,
    copy: {
      title: 'Cookies on example.com',
      description:
        'We use cookies to measure what works. Choose what you allow.',
      acceptAll: 'Allow all',
      rejectAll: 'Essential only',
    },
  },
})
typescript

Keys: title, description, acceptAll, rejectAll, preferences, save, privacyPolicy, noticeTitle, noticeDescription, noticeOptOut, dismiss, and per-category categoryEssential, categoryAnalytics, categoryMarketing, categoryAdvertising (each with a matching …Hint).

This is also how you localise the banner: pick the strings for the visitor's language and pass them at init.

#Letting visitors change their mind

Withdrawing consent has to be as easy as giving it, so add a link somewhere permanent (usually the footer):

<button onclick="vt.showConsentBanner()">Cookie settings</button>
html

This reopens the banner with the category checkboxes, whatever the visitor chose before, and works even in regions where the banner never appeared on its own.

#Forcing a regime

If your legal advice is to apply one rule to everybody, skip the per-visitor lookup:

vt.init('YOUR_PROJECT_TOKEN', {
  api_host: 'https://www.vtilt.com',
  // 'auto' (default) resolves per visitor. 'opt-in' | 'opt-out' | 'none'
  // force one everywhere; 'off' disables the banner.
  consent_banner: { enabled: true, mode: 'opt-in' },
})
typescript

mode: 'opt-in' is the safest and highest-friction setting: every visitor must accept before non-essential tracking runs.

#What happens before a visitor answers

In a region that requires consent, vTilt holds events while your banner is open rather than discarding them, and the visitor's answer decides what happens next.

The visitor…What leaves the browserWhat you see in vTilt
hasn't answered yetnothingnothing yet
acceptseverything held, in the order it happenedthe full visit, landing page included
rejectsnothing, evernothing
never answersnothingnothing

So accepting after browsing three pages gives you those three pages, with their original timestamps — not three events dated to the moment they clicked Accept.

Four things worth knowing:

  • Nothing is written to storage while a decision is pending. The queue lives in memory only, which is what keeps it lawful — the data never leaves the device before there is a basis for it. The trade-off: the queue does not survive a page navigation. A visitor who accepts on page three recovers page three, not page one.
  • identify(), alias() and $set are gated too. They carry the most personal data you send us, so they wait for the same answer — and are released by it, so consenting still links the person.
  • The queue holds 50 events. Past that, new ones are dropped rather than the oldest, so the entry pageview and its referrer survive.
  • If you set require_consent: true without a banner, there is nothing to answer, so events are dropped rather than held. Use a banner — or call vt.setConsent() from your own CMP — if you want them recoverable.

Note

in opt-out and unregulated regions nothing is gated, so events flow from the first pageview as normal.

#Running with no banner at all

If you would rather not show a consent banner anywhere, there is one configuration that supports it:

vt.init('YOUR_PROJECT_TOKEN', {
  api_host: 'https://www.vtilt.com',
  persistence: 'memory',
})
typescript

In memory mode vTilt writes nothing to the visitor's device — no cookies, no localStorage, no sessionStorage. That matters more than "no cookies" does. The ePrivacy rule that makes banners necessary in Europe covers storing or reading anything on a device, not cookies specifically, so localStorage counts. Store nothing and the rule is not engaged.

What it costs you, and it is not small:

Defaultmemory
Cookies / local storageanonymous id storednothing stored
Returning visitorsrecognisedevery pageview is a new visitor
identify()links a personcannot link across page loads
Consent banner in the EUrequired in opt-in regionsnot required

So it suits a marketing site where you want clean aggregate traffic without a banner, and does not suit a product where you need funnels, retention, or identified users.

Note

this is about the browser SDK's storage. Your server-side data, and anything you send from the Node SDK, is unaffected.

Important

we can tell you what the SDK stores; we cannot tell you what your obligations are. If you plan to run with no banner, have your own counsel confirm it for your jurisdiction and your use of the data.

#Global Privacy Control

vTilt honours Global Privacy Control automatically. Nothing to enable, and it works whether or not you show a banner.

When a visitor's browser sends the signal — Brave and Firefox include it, DuckDuckGo and several extensions add it — vTilt treats it as an opt-out of sale and sharing:

Without GPCWith GPC
analyticsgrantedgranted
marketinggranteddenied
advertisinggranteddenied

First-party analytics keeps working; ad and marketing signals do not turn on by themselves. This is the same outcome as a visitor pressing Do not sell or share my personal information.

Two rules worth knowing:

  • An explicit choice wins. If a visitor opens the preferences panel and turns advertising on, that sticks — a specific answer outranks a browser-wide default.
  • It applies everywhere, not only in the US states that require it. Twelve states currently mandate honouring a universal opt-out signal, but the signal means the same thing wherever it is sent.

Events carry $gpc: true while the signal is present, so you can see in your own data how much of your traffic sends it — and show that it was honoured.

Note

GPC affects the default. In an opt-in region nothing is granted by default anyway, so the signal changes nothing there.

#Configuring from the dashboard

Project settings → Tracking → Privacy carries the same settings, so you can turn the banner on, switch regimes, or change the privacy-policy link without a deploy:

SettingEquivalent in vt.init()
Show a consent bannerconsent_banner: { enabled: true }
When to askmode
Positionposition
Privacy policy URLprivacyPolicyUrl
Stylingtheme

The Styling fields cover the same appearance, colours, corner radius, font family, and dialog scrim as theme, with a live preview beside them. The preview uses the banner's own stylesheet, so what you see is what visitors get; switch between the consent dialog and the opt-out notice, check the mobile layout, and open the preferences list without publishing anything. Anything you leave at its default keeps following the built-in stylesheet, and Reset styling clears the lot.

For anything finer than those fields — spacing, shadows, individual buttons — use the part selectors shown under Styling. Those are CSS on your own page and are not configurable from the dashboard.

Anything you set in vt.init() wins — the dashboard only fills in what your snippet left out. So a project can enable the banner in the dashboard while pinning mode: 'opt-in' in code, and both apply.

Warning

For the strictest gating, enable the banner in vt.init() as well. Dashboard settings reach the SDK on its first config request, which normally arrives before the first pageview but is not guaranteed to on a slow connection. Passing consent_banner: { enabled: true } closes that window; everything else can still come from the dashboard. After a visitor's first page view in a region that requires consent, the SDK remembers to gate early on subsequent loads regardless.

#Driving consent from your own CMP

Already using OneTrust, Cookiebot, or your own banner? Leave consent_banner off and tell vTilt what the visitor chose:

vt.setConsent({ analytics: true, marketing: false, advertising: false })
typescript

To make vTilt withhold non-essential events until your CMP reports a choice, turn on Require consent under Project settings → Tracking → Privacy. vt.getConsent() returns the current state.

Note

vTilt is not an IAB TCF-certified CMP. If you need TCF, keep your existing CMP and drive vTilt with setConsent() as above.

#Reading the resolved region

vt.getConsentRegion()
// { regime: 'opt-in', country: 'DE', region: null }
// null while the lookup is still in flight
typescript

country is an ISO-3166-1 alpha-2 code; region is the US state code where the regime depended on it.

Warning

If the region lookup is blocked (usually by an ad-blocker), vTilt assumes opt-in and shows the banner. This is deliberate: the safe failure is asking someone who did not need to be asked, not tracking someone who needed to consent first. If the banner bundle itself is also blocked, no banner appears and non-essential events stay withheld for that visitor — the same trade, one step further.

PreviousOutbound messagesBrowser SDKNextFeature readinessBrowser SDK