Skip to content

Privacy

Add a cookie consent banner

Show a consent banner and hold analytics until visitors accept.

Problem

You set non-essential cookies or load analytics, and you need visitors to consent first — with the banner styled to your brand and your trackers genuinely held back until they accept.

UI

  1. In app.lessly.com, open the consent settings for your product.
  2. Add a provider for each tracker you load (for example, your analytics), and place it in a category: necessary, preferences, statistics, or marketing. List the hostnames the tracker loads from.
  3. Optional: set the banner’s brand color, corners, and font so it matches your site.
  4. Copy the embed snippet and paste it into your pages, just before the closing </body> tag.

MCP

The same flow through the Lessly MCP server in your agent. Token scope: a key with write access to your product’s consent settings.

Ask in plain language:

“Add a cookie consent banner to example.com. We load PostHog analytics — put it under statistics.”

Your agent declares the providers, applies your brand theme, and returns the embed snippet:

<script
  src="https://public.lessly.com/<YOUR_PRODUCT_ID>/consent/banner.js"
  data-product-id="<YOUR_PRODUCT_ID>"
></script>

Paste that one line before </body> and deploy. The banner reads its configuration — providers, categories, and theme — from Lessly at load time, so you change the banner in Lessly without redeploying your site.

Verify it works

Open your site in a fresh browser with no prior choice stored:

  1. The banner appears, and your browser’s Network panel shows no requests to your analytics host yet.
  2. Click Allow all.
  3. Your analytics requests now fire.

If analytics fire before you accept, the tracker is loading in a way the banner cannot intercept — see Variations.

Variations

Analytics bundled into your own app. The banner neutralizes third-party <script src> tags by hostname. If your analytics is bundled into your app and started by your own code — a dynamic import() plus fetch — host-based blocking cannot stop it. Gate it yourself on the visitor’s decision: the banner mirrors the choice through Google Consent Mode (gtag('consent', 'update', { analytics_storage })) and replays it on load for returning visitors. Start analytics only when analytics_storage is granted. If you’d rather gate on the choice explicitly, read it with getDecision() and start analytics when statistics is true — see JavaScript API below.

Let visitors reopen their choice. People change their mind. Lessly shows a floating “Cookie settings” entry point by default; you can move it to a corner or bind it to a link you already have in your footer by calling openPreferences() — see JavaScript API below.

JavaScript API

The banner installs a small global, window.LesslyConsent, so your own code can read the visitor’s choice and drive the banner — reopen the settings panel, or gate analytics you bundle yourself.

openPreferences() — open the settings panel so the visitor can change or withdraw their choice. Wire it to your own “Cookie settings” link.

getDecision() — the visitor’s current choice. It returns null before the visitor decides, otherwise a frozen snapshot of the choice at the moment you call it:

{
  preferences: false,
  statistics: true,
  marketing: false,
  method: 'explicit',
  at: 1718900000000,
}

The three category flags are booleans; necessary is always on, so it isn’t listed. method is one of explicit, implied, or withdrawal. at is the decision time in epoch milliseconds, UTC. The snapshot is frozen — call getDecision() again after a change to read the new state.

withdrawAll(options?) — withdraw every non-essential category, clear the matching cookies and storage, then reload the page. In a single-page app, pass { reload: false } and handle navigation yourself:

await window.LesslyConsent.withdrawAll({ reload: false });

Calling before the banner loads. banner.js loads asynchronously, so it may not be ready when your code runs. Push calls onto an array and the banner drains them once it installs — the same pattern analytics snippets use:

<script>
  window.LesslyConsent = window.LesslyConsent || [];
  window.LesslyConsent.push(['openPreferences']);
</script>

Only openPreferences, getDecision, and withdrawAll are drained. A queued getDecision can’t hand a value back — call it directly, once the banner is ready, when you need the result.

TypeScript. The banner ships as a script, not an npm install, so add ambient types in a .d.ts in your project. They mirror the banner’s own API:

export {};

declare global {
  interface Window {
    LesslyConsent?: LesslyConsentApi | PreInitCall[];
  }
}

interface LesslyConsentApi {
  openPreferences(): void;
  getDecision(): ConsentDecision | null;
  withdrawAll(options?: { reload?: boolean }): Promise<void>;
}

type ConsentDecision = Readonly<{
  preferences: boolean;
  statistics: boolean;
  marketing: boolean;
  method: 'explicit' | 'implied' | 'withdrawal';
  at: number;
}>;

type PreInitCall =
  | ['openPreferences']
  | ['getDecision']
  | ['withdrawAll']
  | ['withdrawAll', { reload?: boolean }];

What Lessly receives

The banner talks to Lessly twice: it fetches your banner configuration, and it posts the visitor’s decision. The decision carries the category choices, the method, the visitor’s region, a timestamp, a record id, your product id, and the page’s origin and path only — never the query string or fragment, so tokens or emails in the URL are not captured. Both requests omit credentials, so none of your site’s cookies are sent to Lessly.

Was this page helpful?
Esc

Start typing to search the docs.

navigate select