# Add a cookie consent banner

## 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:

```html
<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:

```js
{
  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:

```js
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:

```html
<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:

```ts
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.
