Building a Next.js cookie consent banner

Aug 27, 2025

To ensure compliance with privacy regulations like GDPR, you may need to ask for consent from users to track them using cookies. PostHog enables you to track users with or without cookies, but you need to set up the logic to ensure you are compliant both ways.

In this tutorial, we build a basic Next.js app, set up PostHog, build a cookie consent banner, and add the logic for users to opt-in or out of tracking cookies.

Don't want to bother with cookies at all? Here's how to use PostHog without cookie banners.

Create a Next.js app and add PostHog

First, once Node is installed, create a Next.js app. Run the command below, select No for TypeScript, Yes for use app router, and the defaults for every other option.

Terminal
npx create-next-app@latest cookie-banner

Next, install PostHog's JavaScript Web SDK:

Terminal
npm install posthog-js

To set up PostHog, create a instrumentation-client.js file in the root of your project. In it, initialize PostHog with your project API key and instance address from your project settings like this:

JavaScript
// instrumentation-client.js
import posthog from 'posthog-js'
posthog.init('<ph_project_api_key>', {
api_host: 'https://us.i.posthog.com',
defaults: '2025-05-24'
});

After setting this up and running npm run dev, PostHog starts autocapturing events, but a PostHog-related cookie is set for the user without their consent.

Cookie set

Ensuring cookies aren't set on initial load

If you want to make sure you are fully compliant, you may want to ensure cookies aren't set until the user has given consent.

To do this, you can set cookieless_mode to on_reject in your initialization config like this:

JavaScript
// instrumentation-client.js
import posthog from "posthog-js";
posthog.init("<ph_project_api_key>", {
api_host: "https://us.i.posthog.com",
defaults: '2025-05-24',
cookieless_mode: 'on_reject'
})

This means that PostHog will not set any cookies until the user has given consent, which is what we rely on the cookie banner to do.

Create another file in the app folder named banner.js for the banner component with a bit of text explaining cookies and buttons to accept or decline.

Importantly, to avoid a hydration error, we must check if the frontend has mounted and only show the component if so. We can use useState and useEffect to do this. Together, this looks like this:

JavaScript
// app/banner.js
'use client';
import { useEffect, useState } from "react";
export function Banner() {
const [consentGiven, setConsentGiven] = useState('');
useEffect(() => {
// We want this to only run once the client loads
// or else it causes a hydration error
setConsentGiven('pending');
}, []);
return (
<div>
{consentGiven === 'pending' && (
<div>
<p>
We use tracking cookies to understand how you use
the product and help us improve it.
Please accept cookies to help us improve.
</p>
<button type="button">Accept cookies</button>
<span> </span>
<button type="button">Decline cookies</button>
</div>
)}
</div>
)
}

After creating this, we import the component into layout.js and set it up inside our body component:

JavaScript
// app/layout.js
import "./globals.css";
import { Banner } from "./banner";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Banner />
</body>
</html>
);
}

This creates an ugly but functional cookie banner at the bottom of our site. You can customize and style it how you want.

Banner

Next, we add the logic to handle and store the user's consent. We do this in banner.js by adding handleAcceptCookies and handleDeclineCookies functions and connect them to PostHog's consent management methods like this:

JavaScript
// app/banner.js
'use client';
import { useEffect, useState } from "react";
import posthog from "posthog-js";
export function Banner() {
const [consentGiven, setConsentGiven] = useState('');
useEffect(() => {
// We want this to only run once the client loads
// or else it causes a hydration error
setConsentGiven(posthog.get_explicit_consent_status());
}, []);
const handleAcceptCookies = () => {
posthog.opt_in_capturing();
setConsentGiven('granted');
};
const handleDeclineCookies = () => {
posthog.opt_out_capturing();
setConsentGiven('denied');
};
return (
<div>
{consentGiven === 'pending' && (
<div>
<p>
We use tracking cookies to understand how you use
the product and help us improve it.
Please accept cookies to help us improve.
</p>
<button type="button" onClick={handleAcceptCookies}>Accept cookies</button>
<span> </span>
<button type="button" onClick={handleDeclineCookies}>Decline cookies</button>
</div>
)}
</div>
);
}

Now, when users make a choice, PostHog opts them in or out of tracking cookies, storing the choice in local storage.

Further reading

Subscribe to our newsletter

Product for Engineers

Read by 60,000+ founders and builders

We'll share your email with Substack

Questions? Ask Max AI.

It's easier than reading through 814 pages of documentation

Comments