ConsentStackDocs

Framework Guides

Step-by-step guides for adding ConsentStack to React, Next.js, Vue, Nuxt, and static sites.

Framework-specific guides for integrating ConsentStack. Each section shows the recommended approach for your stack. Pick the one that matches and you'll be up and running in minutes.

For the full React SDK reference (hooks, props, headless mode), see the React SDK docs. For the vanilla JavaScript API, see the JavaScript API reference.

Next.js (App Router)

Install the React SDK and wrap your root layout with ConsentStackProvider. This gives every page access to consent state via hooks.

npm install @consentstack/react
app/layout.tsx
import { ConsentStackProvider } from '@consentstack/react'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <link rel="preconnect" href="https://cdn.consentstack.io" />
      </head>
      <body>
        <ConsentStackProvider siteKey="<YOUR_SITE_KEY>">
          {children}
        </ConsentStackProvider>
      </body>
    </html>
  )
}

For best performance, also add <link rel="preconnect" href="https://cdn.consentstack.io" /> to the document <head> (shown above) so the TLS connection is warm when the React provider injects the script tags.

Use the useConsent hook in any client component to check consent state or conditionally load scripts:

components/analytics.tsx
'use client'

import { useConsentValue } from '@consentstack/react'
import Script from 'next/script'

export function Analytics() {
  const hasAnalytics = useConsentValue('analytics')
  if (!hasAnalytics) return null

  return <Script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX" strategy="afterInteractive" />
}

Loading third-party trackers in your layout

Do not drop <Script src="..."> or <GoogleTagManager /> into your root layout for trackers without gating them. Next.js renders an SSR'd <link rel="preload" as="script"> for every <Script> and for @next/third-parties components. The browser's preload scanner fetches those URLs before any JavaScript runs, so the request reaches the third party (and sets cookies on its domain) before ConsentStack can intercept it. The script execution itself is still blocked, but the preload fetch leaks.

The simplest fix that works on every page without component-by-component gating is to inline the tracker's bootstrap snippet with dangerouslySetInnerHTML. The standard JS bootstrap pattern (used by Google Tag Manager, HubSpot, and most major trackers) creates a <script> element at runtime via document.createElement('script') and assigns .src. That assignment goes through the SDK's interceptor and is gated by consent. Because the inline <script> itself has no src attribute, Next.js never emits a preload for it.

app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <link rel="preconnect" href="https://cdn.consentstack.io" />
        <script src="https://cdn.consentstack.io/consent.js?k=<YOUR_SITE_KEY>" />
        <script src="https://cdn.consentstack.io/consent-core.js?k=<YOUR_SITE_KEY>" />
      </head>
      <body>
        {children}

        {/* GTM bootstrap (the standard Google snippet) */}
        <script
          dangerouslySetInnerHTML={{
            __html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s);j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-XXXXXXX');`,
          }}
        />

        {/* HubSpot bootstrap */}
        <script
          dangerouslySetInnerHTML={{
            __html: `(function(){var s=document.createElement('script');s.async=true;s.id='hs-script-loader';s.src='//js-na1.hs-scripts.com/<HUB_ID>.js';document.head.appendChild(s);})();`,
          }}
        />
      </body>
    </html>
  )
}

Place these inline scripts after the ConsentStack tags so the SDK's interceptor is installed before the bootstrap runs. When consent is denied, the SDK rewrites the dynamically-created <script> element's src to about:blank and tags it as blocked. When consent later grants, the SDK replays the blocked element and the tracker loads. No third-party request fires before consent.

If you prefer to use <Script> from next/script, gate it on useConsentValue as shown above. The component returning null on the server prevents Next.js from emitting the preload during SSR, and the <Script> only appears in the DOM after consent grants on the client.

Next.js (Pages Router)

Wrap your app in _app.tsx. The provider works the same way, it just lives in a different file.

pages/_app.tsx
import type { AppProps } from 'next/app'
import { ConsentStackProvider } from '@consentstack/react'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <ConsentStackProvider siteKey="<YOUR_SITE_KEY>">
      <Component {...pageProps} />
    </ConsentStackProvider>
  )
}

If you don't need hooks, you can use the simpler ConsentStack component in _document.tsx instead:

pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document'
import { ConsentStack } from '@consentstack/react'

export default function Document() {
  return (
    <Html>
      <Head>
        <link rel="preconnect" href="https://cdn.consentstack.io" />
      </Head>
      <body>
        <Main />
        <NextScript />
        <ConsentStack siteKey="<YOUR_SITE_KEY>" />
      </body>
    </Html>
  )
}

For best performance, add <link rel="preconnect" href="https://cdn.consentstack.io" /> to the document <head> (shown above) so the TLS connection is warm when the React component injects the script tags.

React (Vite / CRA)

Same React SDK, different entry point. Wrap your app at the root.

npm install @consentstack/react
src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ConsentStackProvider } from '@consentstack/react'
import App from './App'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <ConsentStackProvider siteKey="<YOUR_SITE_KEY>">
      <App />
    </ConsentStackProvider>
  </React.StrictMode>
)

For best performance, also add <link rel="preconnect" href="https://cdn.consentstack.io" /> to your index.html <head> so the TLS connection is warm when the React provider injects the script tags.

All hooks (useConsent, useConsentValue) work exactly as documented in the React SDK reference.

Vue & Nuxt

Vue apps use the script tags directly. Add them via useHead in your root layout or app component.

Nuxt 3

Add the preconnect link and script tags globally in nuxt.config.ts:

nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      link: [
        {
          rel: 'preconnect',
          href: 'https://cdn.consentstack.io',
        },
      ],
      script: [
        {
          src: 'https://cdn.consentstack.io/consent.js?k=<YOUR_SITE_KEY>',
          tagPosition: 'head',
        },
        {
          src: 'https://cdn.consentstack.io/consent-core.js?k=<YOUR_SITE_KEY>',
          tagPosition: 'head',
        },
      ],
    },
  },
})

Vue 3 (Vite)

Add the preconnect link and script tags to your index.html:

index.html
<head>
  <link rel="preconnect" href="https://cdn.consentstack.io" />
  <script src="https://cdn.consentstack.io/consent.js?k=<YOUR_SITE_KEY>"></script>
  <script src="https://cdn.consentstack.io/consent-core.js?k=<YOUR_SITE_KEY>"></script>
</head>

Vue composable

Access consent state reactively by wrapping window.consentstack in a composable:

composables/useConsent.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useConsent() {
  const consent = ref<Record<string, boolean> | null>(null)
  let unsubscribe: (() => void) | undefined

  onMounted(() => {
    const sdk = window.consentstack
    if (!sdk) return

    consent.value = sdk.getConsent()
    unsubscribe = sdk.onConsentChange((updated) => {
      consent.value = updated
    })
  })

  onUnmounted(() => {
    unsubscribe?.()
  })

  const hasConsent = (category: string): boolean => {
    return consent.value?.[category] ?? false
  }

  return { consent, hasConsent }
}
components/Analytics.vue
<script setup lang="ts">
import { useConsent } from '@/composables/useConsent'

const { hasConsent } = useConsent()
</script>

<template>
  <div v-if="hasConsent('analytics')">
    <!-- Analytics scripts load here -->
  </div>
</template>

Vanilla JS & Static Sites

The simplest integration. Add the preconnect link and script tags to your HTML <head> and ConsentStack handles everything automatically, no framework, no build step.

<head>
  <link rel="preconnect" href="https://cdn.consentstack.io" />
  <script src="https://cdn.consentstack.io/consent.js?k=<YOUR_SITE_KEY>"></script>
  <script src="https://cdn.consentstack.io/consent-core.js?k=<YOUR_SITE_KEY>"></script>
</head>

To let visitors re-open their preferences (e.g., from a "Cookie Settings" link in your footer), add an anchor that points to #cs-preferences:

<footer>
  <a href="#cs-preferences">Cookie Settings</a>
</footer>

For programmatic access, use the global window.consentstack API:

// Check consent before running a script
if (window.consentstack?.hasConsent('marketing')) {
  loadFacebookPixel()
}

// Listen for consent changes
window.consentstack?.onConsentChange((consent) => {
  console.log('Consent updated:', consent)
})

See the JavaScript API reference for the full API surface.

WordPress

Add the preconnect link and script tags to your site's <head> using the theme customizer or a header/footer plugin.

Theme Customizer: Go to Appearance > Customize > Additional Scripts (if your theme supports it) and paste:

<link rel="preconnect" href="https://cdn.consentstack.io" />
<script src="https://cdn.consentstack.io/consent.js?k=<YOUR_SITE_KEY>"></script>
<script src="https://cdn.consentstack.io/consent-core.js?k=<YOUR_SITE_KEY>"></script>

Header/footer plugin: Install a plugin like WPCode or Insert Headers and Footers. Add all three tags above to the header section.

functions.php: If you prefer code, add this to your theme's functions.php:

functions.php
function consentstack_preconnect() {
  echo '<link rel="preconnect" href="https://cdn.consentstack.io" />';
}
add_action('wp_head', 'consentstack_preconnect', 0);

function consentstack_enqueue_script() {
  wp_enqueue_script(
    'consentstack-stub',
    'https://cdn.consentstack.io/consent.js?k=<YOUR_SITE_KEY>',
    array(),
    null,
    false
  );
  wp_enqueue_script(
    'consentstack-core',
    'https://cdn.consentstack.io/consent-core.js?k=<YOUR_SITE_KEY>',
    array('consentstack-stub'),
    null,
    false
  );
}
add_action('wp_enqueue_scripts', 'consentstack_enqueue_script');

Replace <YOUR_SITE_KEY> with your actual site key from the ConsentStack dashboard. You'll find it under Site Settings after creating your site.

What's next