diff --git a/packages/cookie-banner/README.md b/packages/cookie-banner/README.md
index ffc379ec18..5f8754e021 100644
--- a/packages/cookie-banner/README.md
+++ b/packages/cookie-banner/README.md
@@ -140,6 +140,22 @@ If your site manages Consent Mode itself (for example through an existing GTM se
The attribute accepts `"true"`/`"false"`; any other value logs a warning and keeps the integration enabled. Programmatic use of `CookieBannerClient` takes the same switch as an option: `integrations: [{ name: "gcm", enabled: false }]`. Disabling covers all of it — the eager deny-all default, the discovery-mode grant-all, and per-consent updates.
+## Global Privacy Control
+
+When a visitor's browser sends [Global Privacy Control](https://globalprivacycontrol.org/), the SDK applies an opt-out and records it as a consent decision: a visitor id in `localStorage`, the consent cookie, and a consent record sent to Probo. That happens even when no banner UI is mounted, which is surprising for a detection-only embed, and it means a second consent record exists for sites that already have their own.
+
+`gpc-record="false"` stops the SDK persisting and transmitting that decision:
+
+```html
+
+
+
+
+
+```
+
+**GPC is still honoured.** The opt-out is applied in the page exactly as before — non-necessary categories are denied, blocked resources stay blocked, and integrations are updated — so the flag cannot be used to ignore a GPC signal. What it removes is the record of it: no visitor id, no cookie, no consent record. Programmatic equivalent: `gpcRecord: false`.
+
## Key Features
- **Multi-regulation compliance** — Supports opt-in (GDPR, ePrivacy) and opt-out (CCPA/CPRA) consent modes, Global Privacy Control (GPC) detection, and per-category cookie controls. Under CCPA the banner starts closed; the settings link (“Your Privacy Choices” + official icon) opens the Privacy Choices panel with sale/sharing opt-out and an SPI rights statement.
diff --git a/packages/cookie-banner/src/attributes.test.ts b/packages/cookie-banner/src/attributes.test.ts
new file mode 100644
index 0000000000..a7bc718397
--- /dev/null
+++ b/packages/cookie-banner/src/attributes.test.ts
@@ -0,0 +1,51 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { resolveBooleanAttribute } from "./attributes";
+
+describe("resolveBooleanAttribute", () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it("defaults to true when the attribute is absent", () => {
+ expect(resolveBooleanAttribute(null, "gpc-record")).toBe(true);
+ });
+
+ it("parses \"true\" and \"false\"", () => {
+ expect(resolveBooleanAttribute("true", "gpc-record")).toBe(true);
+ expect(resolveBooleanAttribute("false", "gpc-record")).toBe(false);
+ });
+
+ it("normalizes case and whitespace", () => {
+ expect(resolveBooleanAttribute("FALSE", "gpc-record")).toBe(false);
+ expect(resolveBooleanAttribute(" false ", "gpc-record")).toBe(false);
+ expect(resolveBooleanAttribute("True", "gpc-record")).toBe(true);
+ });
+
+ it("warns with the attribute name and falls back to true", () => {
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
+ expect(resolveBooleanAttribute("off", "gpc-record")).toBe(true);
+ expect(warn).toHaveBeenCalledTimes(1);
+ expect(warn.mock.calls[0][0]).toContain("gpc-record");
+ });
+});
diff --git a/packages/cookie-banner/src/attributes.ts b/packages/cookie-banner/src/attributes.ts
new file mode 100644
index 0000000000..b094048e1f
--- /dev/null
+++ b/packages/cookie-banner/src/attributes.ts
@@ -0,0 +1,41 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+export function resolveBooleanAttribute(
+ value: string | null,
+ attribute: string,
+): boolean {
+ if (value == null) {
+ return true;
+ }
+
+ const normalized = value.trim().toLowerCase();
+ if (normalized === "true") {
+ return true;
+ }
+ if (normalized === "false") {
+ return false;
+ }
+
+ console.warn(
+ `[probo] invalid ${attribute} value "${value}": expected "true" or "false", falling back to enabled`,
+ );
+ return true;
+}
diff --git a/packages/cookie-banner/src/client.ts b/packages/cookie-banner/src/client.ts
index 0d9a18beae..e47e1e2655 100644
--- a/packages/cookie-banner/src/client.ts
+++ b/packages/cookie-banner/src/client.ts
@@ -79,6 +79,7 @@ export class CookieBannerClient {
private readonly lang: string;
private readonly integrations: ConsentIntegration[];
+ private readonly gpcRecord: boolean;
private bannerConfig: BannerConfig | null = null;
private consent: VisitorConsent | null = null;
@@ -97,6 +98,7 @@ export class CookieBannerClient {
this.visitorId = getVisitorId(config.bannerId);
this.lang = detectLanguage(config.lang);
this.integrations = createDefaultIntegrations(config.integrations);
+ this.gpcRecord = config.gpcRecord !== false;
}
get loaded(): boolean {
@@ -194,7 +196,13 @@ export class CookieBannerClient {
gpcData[cat.slug] = cat.kind === "NECESSARY";
}
getConsent()._setReady(gpcData, false);
- this.gpc();
+ // GPC is honoured either way; gpcRecord only controls whether the
+ // decision is persisted and sent.
+ if (this.gpcRecord) {
+ this.gpc();
+ } else {
+ this.activate(gpcData);
+ }
this._gpcApplied = true;
} else if (!this.consent) {
const defaults = this.buildDefaultConsentData();
diff --git a/packages/cookie-banner/src/components/cookie-banner-root.ts b/packages/cookie-banner/src/components/cookie-banner-root.ts
index 5af666c795..bd21c83acb 100644
--- a/packages/cookie-banner/src/components/cookie-banner-root.ts
+++ b/packages/cookie-banner/src/components/cookie-banner-root.ts
@@ -18,6 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
+import { resolveBooleanAttribute } from "../attributes";
import { CookieBannerClient } from "../client";
import { resolveGcmEnabled } from "../integrations";
import { resolveLayout } from "../layout";
@@ -146,12 +147,14 @@ export class ProboCookieBannerRoot extends ProboElement implements ProboRootElem
const lang = this.getAttribute("lang") ?? undefined;
const gcmEnabled = resolveGcmEnabled(this.getAttribute("gcm-enabled"));
+ const gpcRecord = resolveBooleanAttribute(this.getAttribute("gpc-record"), "gpc-record");
this._client = new CookieBannerClient({
bannerId,
baseUrl,
lang,
integrations: [{ name: "gcm", enabled: gcmEnabled }],
+ gpcRecord,
});
try {
@@ -179,7 +182,9 @@ export class ProboCookieBannerRoot extends ProboElement implements ProboRootElem
this.scheduleValidation(() => this.validateSettingsLink());
- if (this._client.hasConsent) {
+ // gpcApplied covers the opt-out being in effect without a stored consent,
+ // which is the gpcRecord: false case.
+ if (this._client.hasConsent || this._client.gpcApplied) {
this.setState("hidden");
} else {
this.setState(resolveLayout(this._config).initial_state);
diff --git a/packages/cookie-banner/src/integrations/index.ts b/packages/cookie-banner/src/integrations/index.ts
index 05eb5b2cd4..5d3028b7e0 100644
--- a/packages/cookie-banner/src/integrations/index.ts
+++ b/packages/cookie-banner/src/integrations/index.ts
@@ -21,6 +21,7 @@
export type { ConsentIntegration } from "./integration";
export { GoogleConsentModeIntegration } from "./gcm";
+import { resolveBooleanAttribute } from "../attributes";
import type { IntegrationConfig } from "../types";
import type { ConsentIntegration } from "./integration";
import { GoogleConsentModeIntegration } from "./gcm";
@@ -39,20 +40,5 @@ export function createDefaultIntegrations(
}
export function resolveGcmEnabled(value: string | null): boolean {
- if (value == null) {
- return true;
- }
-
- const normalized = value.trim().toLowerCase();
- if (normalized === "true") {
- return true;
- }
- if (normalized === "false") {
- return false;
- }
-
- console.warn(
- `[probo] invalid gcm-enabled value "${value}": expected "true" or "false", falling back to enabled`,
- );
- return true;
+ return resolveBooleanAttribute(value, "gcm-enabled");
}
diff --git a/packages/cookie-banner/src/themed-banner/iife.ts b/packages/cookie-banner/src/themed-banner/iife.ts
index 22a12178be..f7a5d98edf 100644
--- a/packages/cookie-banner/src/themed-banner/iife.ts
+++ b/packages/cookie-banner/src/themed-banner/iife.ts
@@ -56,6 +56,11 @@ if (script) {
el.setAttribute("gcm-enabled", gcm);
}
+ const gpc = script.getAttribute("data-gpc-record");
+ if (gpc) {
+ el.setAttribute("gpc-record", gpc);
+ }
+
document.body.appendChild(el);
};
diff --git a/packages/cookie-banner/src/themed-banner/themed-banner.ts b/packages/cookie-banner/src/themed-banner/themed-banner.ts
index ac7ea47f62..8b86756ae1 100644
--- a/packages/cookie-banner/src/themed-banner/themed-banner.ts
+++ b/packages/cookie-banner/src/themed-banner/themed-banner.ts
@@ -64,9 +64,12 @@ export class ProboThemedBanner extends HTMLElement {
const gcm = this.getAttribute("gcm-enabled");
const gcmAttr = gcm ? ` gcm-enabled="${esc(gcm)}"` : "";
+ const gpc = this.getAttribute("gpc-record");
+ const gpcAttr = gpc ? ` gpc-record="${esc(gpc)}"` : "";
+
this.shadow.innerHTML = `
-
+
`;
const root = this.shadow.querySelector("probo-cookie-banner-root") as ProboCookieBannerRoot;
@@ -117,7 +120,9 @@ export class ProboThemedBanner extends HTMLElement {
this.shadow.querySelector("[data-action=back]")?.addEventListener("click", () => {
root.setState(
- root.client.hasConsent ? "hidden" : (root.layout?.initial_state ?? "banner"),
+ root.client.hasConsent || root.client.gpcApplied
+ ? "hidden"
+ : (root.layout?.initial_state ?? "banner"),
);
});
diff --git a/packages/cookie-banner/src/types.ts b/packages/cookie-banner/src/types.ts
index 8ab60dfbbc..9b564db233 100644
--- a/packages/cookie-banner/src/types.ts
+++ b/packages/cookie-banner/src/types.ts
@@ -127,4 +127,5 @@ export interface CookieBannerClientOptions {
baseUrl: string;
lang?: string;
integrations?: IntegrationConfig[];
+ gpcRecord?: boolean;
}