On this page
Cookie consent
Last updated
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 in | Regime | What they see |
|---|---|---|
| EU/EEA, UK | opt-in | A 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; Brazil | opt-out | No banner. A persistent Do not sell or share my personal information link after your page content. Tracking is not interrupted. |
| Everywhere else | none | Nothing. The banner code is never even downloaded. |
#Enable
vt.init('YOUR_PROJECT_TOKEN', {
api_host: 'https://www.vtilt.com',
consent_banner: { enabled: true },
})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.
#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-inregions, 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, andanalytics_storageare set from the visitor's choice. - Meta cookies —
_fbp/_fbcare 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,
},
},
})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;
}Rename it with the noticeOptOut copy key if you prefer "Your privacy choices" — the default is the wording the statute names.
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:
| Value | Behaviour |
|---|---|
auto (default) | Follows the visitor's device setting, via prefers-color-scheme |
light | Always light |
dark | Always 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.
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;
}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;
}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',
},
},
})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>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' },
})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 browser | What you see in vTilt |
|---|---|---|
| hasn't answered yet | nothing | nothing yet |
| accepts | everything held, in the order it happened | the full visit, landing page included |
| rejects | nothing, ever | nothing |
| never answers | nothing | nothing |
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$setare 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: truewithout a banner, there is nothing to answer, so events are dropped rather than held. Use a banner — or callvt.setConsent()from your own CMP — if you want them recoverable.
#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',
})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:
| Default | memory | |
|---|---|---|
| Cookies / local storage | anonymous id stored | nothing stored |
| Returning visitors | recognised | every pageview is a new visitor |
identify() | links a person | cannot link across page loads |
| Consent banner in the EU | required in opt-in regions | not 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.
#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 GPC | With GPC | |
|---|---|---|
analytics | granted | granted |
marketing | granted | denied |
advertising | granted | denied |
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.
#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:
| Setting | Equivalent in vt.init() |
|---|---|
| Show a consent banner | consent_banner: { enabled: true } |
| When to ask | mode |
| Position | position |
| Privacy policy URL | privacyPolicyUrl |
| Styling | theme |
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.
#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 })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.
#Reading the resolved region
vt.getConsentRegion()
// { regime: 'opt-in', country: 'DE', region: null }
// null while the lookup is still in flightcountry is an ISO-3166-1 alpha-2 code; region is the US state code where the regime depended on it.