c15t is a consent manager that ships as npm packages instead of a third-party snippet, which makes it the only open-source option that gives you Google Consent Mode v2 and IAB TCF 2.3 without hand-wiring either. Apache-2.0, roughly 2,100 sites including expo.dev and zed.dev. The trade-off is that the durable half wants a backend, and the offline mode that avoids it gives up audit history and jurisdiction detection. Installation below, then how to verify it actually blocks anything.
Every other open-source consent manager is a script you put on a page. c15t is a dependency you import. That sounds like a detail and it is actually the whole design: consent state lives in your bundle, in your repo, under your review process, and your tags are gated by a loader that knows about it rather than by an attribute a build step might drop.
This is the setup guide. For how it compares to Klaro, CookieConsent, tarteaucitron and Osano, start with the open-source CMP comparison.
When it is the right choice
Pick c15t if you run React or Next.js, you want Consent Mode v2 without writing gtag('consent', ...) calls yourself, you need the IAB TCF for programmatic advertising, or you want consent state available to application code rather than trapped inside a vendor widget.
Do not pick c15t if your site is WordPress, plain HTML, or anything without a JavaScript build. There is a framework-agnostic package, but the value is concentrated in the React and Next.js integrations. For a marketing site that needs a banner and nothing else, this is more machinery than the job requires — CookieConsent will be less work.
Installation
There is a guided CLI path that generates configuration for you:
npx @c15t/cli
Or install directly. For Next.js:
npm install @c15t/nextjs
Import the prebuilt stylesheet in your app-level CSS entrypoint. Skip this only if you are going fully headless or writing your own styles:
/* app/globals.css */
@import "@c15t/nextjs/styles.css";
Create the provider
The provider is a client component — it owns browser state, so it needs the 'use client' boundary:
// components/consent-manager/provider.tsx
'use client';
import { type ReactNode } from 'react';
import {
ConsentManagerProvider,
ConsentBanner,
ConsentDialog,
} from '@c15t/nextjs';
export default function ConsentManagerClient({ children }: { children: ReactNode }) {
return (
<ConsentManagerProvider
options={{
mode: 'hosted',
backendURL: 'https://your-instance.c15t.dev',
consentCategories: ['necessary', 'measurement', 'marketing'],
// Forces the banner to show while developing. Remove for production.
overrides: { country: 'DE' },
}}
>
<ConsentBanner />
<ConsentDialog />
{children}
</ConsentManagerProvider>
);
}
That overrides: { country: 'DE' } is a development convenience — it pins the jurisdiction so the banner appears instead of being suppressed because you are testing from somewhere it is not required. Ship it to production and every visitor gets treated as German. The docs flag it; people still forget.
Then mount it at the app root so every route can read consent state:
// app/layout.tsx
import { ConsentManager } from '@/components/consent-manager';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<ConsentManager>{children}</ConsentManager>
</body>
</html>
);
}
Choosing a mode
Three options, and this is the decision that has consequences:
hosted— a backend resolves jurisdiction and policy, keeps durable consent records, and re-syncs after temporary network failures. What the docs recommend for production.offline— consent stored locally in the browser, no backend. You give up audit history, server-side consent awareness and automatic jurisdiction detection. Fine for development, previews and static deploys; think hard before production.custom— bring your own backend, which you can also self-host with@c15t/backendand your own database.
The honest read: if you chose open source to avoid depending on someone else’s infrastructure, offline is the mode that matches that instinct and it is also the weakest one. custom with a self-hosted backend is the configuration that actually delivers what you came for, and it is real operational work. Price that in before you commit.
Gating scripts
Here is where it diverges most from everything else. There is no type="text/plain" attribute to remember. You declare your scripts on the provider and the loader decides when they run:
'use client';
import { ConsentManagerProvider } from '@c15t/nextjs';
import { gtag } from '@c15t/scripts/google-tag';
import { metaPixel } from '@c15t/scripts/meta-pixel';
const scripts = [
gtag({ id: 'G-XXXXXXX', category: 'measurement' }),
metaPixel({ pixelId: '123456' }),
{
id: 'custom-analytics',
src: 'https://cdn.example.com/analytics.js',
category: 'measurement',
},
];
Helpers from @c15t/scripts return plain script objects, so prebuilt integrations and your own one-off scripts live in the same array. There are prebuilt loaders for several dozen tools — GTM, GA4, Meta, TikTok, LinkedIn, PostHog, Clarity, Hotjar, Intercom and more.
Two things to get right. The loader runs in the browser, so scripts go through provider options rather than being injected from Server Components. And category is a real decision, not a label: use measurement for analytics, marketing for advertising and conversion tracking. Mislabel an ad pixel as measurement and you are firing it for people who refused marketing.
For application code that needs to branch on consent, there is a hook:
import { useConsentManager } from '@c15t/nextjs';
function AnalyticsLoader() {
const { has } = useConsentManager();
if (has('measurement')) {
// safe to load
}
return null;
}
Consent Mode v2, which you do not have to wire
This is the reason to look at c15t if you use Google’s stack. Its documentation is unambiguous: it initialises Google Tag with Consent Mode v2 defaults set to denied and updates consent state automatically when users choose. You do not configure Consent Mode yourself.
Compare that with what the same job takes elsewhere. With Klaro you attach a watcher and map categories to the seven signals by hand. With CookieConsent you write the gtag('consent', ...) calls, and its own docs never mention wait_for_update, ads_data_redaction or url_passthrough. Both are perfectly doable and both are where implementations silently end up on v1 — setting ad_storage and analytics_storage while quietly omitting ad_user_data and ad_personalization, which degrades remarketing audiences without any visible error.
If you want the TCF as well, @c15t/iab adds TCF 2.3 with TC string generation and Global Vendor List support. One caveat worth stating plainly: implementing the TCF is not the same as being certified by Google, which is a separate requirement for serving personalised ads in the EEA and UK. c15t’s docs make no certification claim, so if you are monetising European traffic, check that gate yourself — we cover why it matters here.
Verify it actually blocks something
Installing a consent manager and seeing a banner appear tells you the banner works. It tells you nothing about whether anything is being blocked, and those are different claims.
Start with DevTools, which gives you a runtime inspector while building:
npm install @c15t/dev-tools
import { DevTools } from '@c15t/dev-tools/react';
// inside the provider
{process.env.NODE_ENV !== 'production' && <DevTools />}
Then check it from outside the framework, because the failure you are looking for is precisely the one the framework cannot see. Open the network tab in a clean profile, load the page, and refuse everything. Nothing should reach Google, Meta or any analytics endpoint. Anything that does is a tag that never went through the provider — a script pasted into a layout, a third-party component loading its own dependency, a marketing tag someone added in GTM.
That last category is the one that gets sites sued, and no amount of correct c15t configuration prevents it, because the tag was never c15t’s to gate. Which is why the check has to be continuous rather than a launch-day box: run a scan to see exactly what fires before and after consent, and keep it running so the deploy that adds an ungated pixel does not go unnoticed for six months.
The honest scorecard
What is genuinely good: Consent Mode v2 handled for you. The TCF available at all, which no other open-source option offers. Consent state as application state, so gating a React component is as easy as gating a script. Active development — the most active in this space right now. Apache-2.0. And no third-party blocking script in front of your site.
What to weigh: the full-strength configuration wants a backend, and the mode that avoids one is meaningfully weaker. It is young next to projects that have been running since 2013, so there is less accumulated edge-case handling. It is React-shaped, so a non-JS stack gets little from it. And no certification claim, which matters if and only if you monetise European traffic with Google.
Bottom line: if you are on Next.js and you use Google’s measurement stack, this is the strongest open-source option available today, and the Consent Mode v2 handling alone saves the class of silent misconfiguration we see most often. If you are on WordPress or a static site, it is not for you, and one of the other four will be.
Either way, the banner is the promise. The scan is the proof.