ConsentStackDocs

Headless Mode

Use ConsentStack's consent engine without the default banner UI. Build your own consent experience.

Headless mode gives you ConsentStack's full consent engine — config fetching, script blocking, consent storage, platform signaling, and compliance logging — without rendering any UI. You build the consent experience yourself.

When to use headless mode

Headless mode is the right choice when you need:

  • A fully custom-branded consent experience that matches your app's design system
  • Consent in a mobile app webview where native controls replace a web banner
  • Server-rendered consent flows where you control the HTML and hydration
  • Embedded consent in existing UI like a settings page or onboarding wizard

If the default banner works for you, stick with the standard mode. You get the same compliance engine either way.

How to enable it

Add data-mode="headless" to the SDK script tag:

<script
  src="https://cdn.consentstack.io/consent.js"
  data-site-key="YOUR_SITE_KEY"
  data-mode="headless"
  defer
></script>

That's it. The SDK initializes normally — fetches your config, loads existing consent from storage, starts blocking scripts — but skips all banner, preferences panel, and re-entry button rendering.

What still works

Everything except the visual UI:

FeatureStandard modeHeadless mode
Config fetching & cachingYesYes
Geo-detection & region resolutionYesYes
Script blockingYesYes
Platform adapters (Google, Meta, etc.)YesYes
Consent storageYesYes
Consent logging to serverYesYes
Offline queueYesYes
Banner & preferences UIYesNo
Re-entry buttonYesNo

The full JavaScript API is available on window.consentstack, so you can read state, set consent, and listen for events — exactly as you would in standard mode.

Building a custom UI

The workflow is straightforward: wait for the SDK to be ready, read the config to know which categories to display, render your UI, and call setConsent() when the visitor makes a choice.

Step 1: Listen for the "ready" event

The SDK emits a ready event once it has fetched the config and loaded any existing consent from storage:

window.consentstack.on("ready", ({ config, consent }) => {
  // config — your consent configuration (categories, content, etc.)
  // consent — existing consent (null for first-time visitors)
  renderMyConsentUI(config, consent)
})

Step 2: Read categories from the config

The config contains the consent categories you defined in the dashboard. Each category has an id, name, description, and required flag:

const config = window.consentstack.getConfig()

for (const category of config.categories) {
  console.log(category.id)          // "analytics"
  console.log(category.name)        // "Analytics"
  console.log(category.description) // "Help us understand how visitors use our site"
  console.log(category.required)    // true for essential categories
}

Step 3: Render your UI

Build whatever UI fits your product. A modal, a bottom sheet, an inline form — there are no constraints. Just display the categories and let the visitor toggle them.

When the visitor makes a choice, pass a categories object to setConsent(). Each key is a category ID, each value is true (granted) or false (denied):

await window.consentstack.setConsent({
  essential: true,
  analytics: true,
  marketing: false,
})

This single call does everything: saves consent to storage, logs it to the server, updates script blocking, and fires platform adapter signals (Google Consent Mode, Meta CAPI, etc.).

Full example

Here's a minimal custom banner using vanilla JavaScript:

window.consentstack.on("ready", ({ config, consent }) => {
  // Returning visitor — consent already given
  if (consent) return

  const banner = document.createElement("div")
  banner.id = "my-consent-banner"
  banner.innerHTML = `
    <h3>${config.content.title}</h3>
    <p>${config.content.description}</p>
    <button id="my-accept">Accept all</button>
    <button id="my-reject">Reject all</button>
  `
  document.body.appendChild(banner)

  document.getElementById("my-accept").addEventListener("click", async () => {
    const categories = {}
    for (const cat of config.categories) categories[cat.id] = true
    await window.consentstack.setConsent(categories)
    banner.remove()
  })

  document.getElementById("my-reject").addEventListener("click", async () => {
    const categories = {}
    for (const cat of config.categories) {
      categories[cat.id] = cat.required // only essential stays true
    }
    await window.consentstack.setConsent(categories)
    banner.remove()
  })
})

React integration

The @consentstack/react package supports headless mode natively. Set mode="headless" on the provider and use the useConsent hook to build your UI:

import { ConsentStackProvider, useConsent } from "@consentstack/react"

function App() {
  return (
    <ConsentStackProvider siteKey="YOUR_SITE_KEY" mode="headless">
      <MyConsentBanner />
      <RestOfYourApp />
    </ConsentStackProvider>
  )
}

function MyConsentBanner() {
  const { consent, config, isReady, setConsent } = useConsent()

  // Not ready yet or consent already given
  if (!isReady || consent) return null

  const acceptAll = async () => {
    const categories: Record<string, boolean> = {}
    for (const cat of config!.categories) categories[cat.id] = true
    await setConsent(categories)
  }

  const rejectAll = async () => {
    const categories: Record<string, boolean> = {}
    for (const cat of config!.categories) {
      categories[cat.id] = cat.required
    }
    await setConsent(categories)
  }

  return (
    <div className="my-consent-banner">
      <h3>We value your privacy</h3>
      <div className="categories">
        {config!.categories.map((cat) => (
          <label key={cat.id}>
            <input
              type="checkbox"
              disabled={cat.required}
              defaultChecked={cat.required || cat.default}
            />
            {cat.name}
          </label>
        ))}
      </div>
      <button onClick={acceptAll}>Accept all</button>
      <button onClick={rejectAll}>Reject all</button>
    </div>
  )
}

The useConsent hook re-renders your component whenever consent state changes, so your UI stays in sync automatically.

You can also use useConsentValue("analytics") to check a single category — it only re-renders when that specific category changes.

Listening for changes

Whether you use vanilla JS or React, you can listen for consent changes anywhere in your app:

const unsubscribe = window.consentstack.onConsentChange((consent) => {
  console.log("Consent updated:", consent)
})

This is useful for updating UI outside your consent banner — for example, showing a "preferences saved" confirmation or conditionally loading a component.

What's next