Skip to content

Next.js (App Router)

The provider and all components are client components. Put them in their own file:

app/providers.tsx
"use client";
import {
ConsentBanner,
type ConsentState,
PermitoProvider,
PreferenceCenter,
PreferencesButton,
} from "@permito/react";
import type { ReactNode } from "react";
export function Providers({
initialState,
children,
}: {
initialState: ConsentState | null;
children: ReactNode;
}) {
return (
<PermitoProvider
config={{
consentVersion: "2026-10",
language: "de-CH",
initialState,
categories: [
{ id: "necessary", required: true },
{ id: "statistics" },
{ id: "marketing" },
],
services: [
{ id: "plausible", name: "Plausible", category: "statistics" },
{ id: "youtube", name: "YouTube", provider: "Google Ireland Ltd.", category: "marketing" },
],
}}
privacyPolicyUrl="/datenschutz"
googleConsentMode
>
{children}
<ConsentBanner />
<PreferenceCenter />
<PreferencesButton />
</PermitoProvider>
);
}

Read the consent cookie on the server and pass it down. Import server helpers from @permito/react/server, which has no "use client" directive.

app/layout.tsx
import "@permito/react/styles.css";
import { getConsentModeDefaultScript, readConsentFromCookieHeader } from "@permito/react/server";
import { headers } from "next/headers";
import type { ReactNode } from "react";
import { Providers } from "./providers";
export default async function RootLayout({ children }: { children: ReactNode }) {
const initialState = readConsentFromCookieHeader((await headers()).get("cookie"));
return (
<html lang="de-CH">
<head>
<script dangerouslySetInnerHTML={{ __html: getConsentModeDefaultScript() }} />
</head>
<body>
<Providers initialState={initialState}>{children}</Providers>
</body>
</html>
);
}

Reading the cookie makes the route dynamic. If you prefer static pages, leave out initialState: gated content then appears right after hydration instead.

app/page.tsx
import { ConsentGate, ConsentIframe, ConsentScript } from "@permito/react";
export default function Page() {
return (
<>
<ConsentScript service="plausible" src="https://plausible.io/js/script.js" defer
attributes={{ "data-domain": "example.com" }} />
<ConsentGate category="statistics" fallback={<p>Statistics are off.</p>}>
<p>Statistics are on.</p>
</ConsentGate>
<ConsentIframe service="youtube" title="Product video"
src="https://www.youtube-nocookie.com/embed/VIDEO_ID" width={560} height={315} />
</>
);
}

Components from @permito/react can be used directly in server components; they are client components under the hood.

If you use a nonce-based CSP, pass the nonce to the inline Consent Mode script and to ConsentScript (nonce prop) or to blockedElements={{ nonce }}.

A complete, tested example lives in examples/nextjs.