Skip to main content

Next.js Customization

Customize

See the designs

See the design gallery for five banner designs, from a bottom bar to a fully custom one. Its React code works in Next.js with the same components imported from c15t/next.

Load the stylesheet yourself

A Next.js app imports no c15t stylesheet. ConsentBanner, ConsentDialog and the other stock components render the rules they use as <style> elements, in the server HTML for a server-rendered banner.

Import the stylesheet yourself with Tailwind CSS 3, to put c15t's rules in a named cascade layer. Set styles: false under options in c15t.config.ts, so the components add no second copy:

c15t.config.ts
import { defineConsentConfig } from 'c15t/next';

export default defineConsentConfig({ options: { styles: false } });

Then import c15t once from the global stylesheet that app/layout.tsx or pages/_app.tsx loads:

styles/globals.css
@import 'c15t/next/styles.css';

c15t's rules live in the cascade layer components, and each <style> element the components render opens with Tailwind CSS 4's layer order, @layer properties, theme, base, components, utilities;. Unlayered CSS, Tailwind 4 utilities and StyleX's atomic classes override c15t's rules without specificity tricks, whichever stylesheet loads first. Stylesheets and CSS layers explains the order and which file holds the dialog's rules.

Tailwind CSS 3 has no cascade layers, and its unlayered preflight beats c15t's layered rules. Set styles: false in the provider options, import styles.css above the @tailwind directives, and add the c15t/postcss-tailwind3 PostCSS plugin before tailwindcss. Utilities you pass to c15t parts then need the important modifier, such as !bg-red-500. Tailwind CSS shows the tested setup for each framework.

Change colors and radius with tokens

Colors, radii, shadows, typography, and motion come from --c15t-* custom properties. The rules the components render set the default values, as does styles.css when you import it with styles: false. To change them, render ConsentTheme with your theme where your app renders on the server, or override the variables directly in your CSS.

ConsentTheme renders a <style id="c15t-theme"> element with the variables for theme. It is not a client component: rendered from a Server Component or another server-only file, the theme generator stays out of the browser bundle. Rendered from a client component it still works, but the browser downloads the generator (about 1.7 KB gzip). The provider no longer turns theme tokens into CSS. It still reads consentActions and slot styles from its theme option, and warns in development when theme holds tokens but the page has no c15t-theme stylesheet.

ConsentTheme propPurpose
themeColors, dark colors, typography, spacing, radius, shadows and motion. Defaults to the built-in theme.
colorScheme'dark' makes the dark tokens the default before hydration. 'system' follows prefers-color-scheme with a CSS media query. Omit it to follow a dark or c15t-dark class on <html>.
nonceContent Security Policy nonce for the <style> element.

Pass the same colorScheme to ConsentTheme and to the provider. If your app manages dark mode through a root class, such as next-themes' dark class, omit colorScheme and include that class in the server HTML. Dark mode covers theme.dark, null and a dark first paint.

To switch between different token sets at runtime, render ConsentTheme from a client component and change its props, which ships the generator.

Outside a component, call generateThemeCSS(theme, colorScheme) from c15t/react/utils, or @c15t/react/utils with the scoped package. It writes the same CSS as ConsentTheme. Outside React, import it from @c15t/ui/theme. Call it on the server or at build time and put the result in a <style> element or your stylesheet. It escapes <, so the result is safe inside <style>.

Define the theme in a module without 'use client', so Server and Client Components can both import it:

lib/theme.ts
import { defineTheme } from 'c15t/next';

export const brandTheme = defineTheme({
	colors: { primary: '#315c47', primaryHover: '#24473a' },
	radius: { lg: '16px' },
});

Render ConsentTheme from a Server Component layout. This layout themes the routes under app/branded:

app/branded/layout.tsx
import { ConsentTheme } from 'c15t/next';
import type { ReactNode } from 'react';

import { brandTheme } from '@/lib/theme';

const BrandedLayout = ({ children }: { children: ReactNode }) => (
	<>
		<ConsentTheme theme={brandTheme} />
		{children}
	</>
);

export default BrandedLayout;

To theme the whole site, render <ConsentTheme theme={brandTheme} /> in your root layout, before ConsentRoot. View the page source to check that the <style id="c15t-theme"> element is in the server HTML. In the Pages Router, render ConsentTheme in pages/_document.tsx, which runs only on the server.

Earlier v3 alphas generated theme CSS in the browser from the provider's theme option. To migrate:

  1. Move the theme into a module without 'use client', so server and client files can both import it.
  2. Render <ConsentTheme theme={theme} /> next to the consent root, from a Server Component or another server-rendered file. Pass colorScheme and nonce if you set them on the provider.
  3. Keep passing theme to the provider only for consentActions and slot styles. A theme with tokens only can be removed from the provider.
  4. The default tokens the provider used to inject now come with the rules the components render. If you import styles.css with styles: false, keep that import.

Style the action buttons

consentActions decides how each action role looks: default applies to every button, primary to whichever actions the policy or your props mark primary, and accept, reject, customize, and dismiss to one role each. Per-action keys win over primary, which wins over default.

Banner sizing has its own variables. Override them in CSS when a variant needs a different footprint:

VariableDefaultApplies to
--consent-banner-max-width440pxfloating cards
--consent-banner-widget-max-width20remwidget chips
--consent-banner-wall-max-width30remwall cards

The footer lays out its actions by the card's width, not the viewport's. With the default compact profile, a card narrower than 22rem puts Reject and Accept on one row and Customize on a full-width row below them.

The primary action can change while the brand color stays the same. Set consentActions.primary once, then select primaryButton per policy as shown in ConsentBanner.

To give opt-in and opt-out banners different colors, scope the tokens to the root's attributes. Include hover and foreground colors when overriding CSS tokens directly:

[data-prompt][data-model='opt-in'] {
	--c15t-primary: #2f6f4e;
	--c15t-primary-hover: #24563c;
	--c15t-text-on-primary: #fff;
}

[data-prompt][data-model='opt-out'] {
	--c15t-primary: #6b3fa0;
	--c15t-primary-hover: #55327f;
	--c15t-text-on-primary: #fff;
}

These colors apply to the action marked primary. Button layout and color changes do not change the policy or expire a saved choice.

Slots

Each component part is a slot. A slot accepts any attributes the element takes, so the contract is the same whether you write class strings or pass an object with className and style. Set slots on the provider under components, keyed by component and part.

ConsentTheme renders tokens only. Components read consentActions in the browser, so pass a theme that contains them through options in c15t.config.ts:

lib/theme.ts (partial)
export const actionTheme = defineTheme({
	consentActions: {
		primary: { variant: 'primary', mode: 'filled' },
		dismiss: { variant: 'neutral', mode: 'stroke' },
	},
});
c15t.config.ts
import { defineConsentConfig } from 'c15t/next';

import { actionTheme } from './lib/theme';

export default defineConsentConfig({ options: { theme: actionTheme } });

Keep your scripts and other options in the same call.

Change the banner's shape

For a full-width notice, use the bar variant. In a Client Component inside the existing root, replace the stock banner with:

components/regional-banner.tsx
'use client';

import { ConsentBanner, usePolicyRule } from 'c15t/next';

export function RegionalBanner() {
	const policy = usePolicyRule();
	return (
		<ConsentBanner variant={policy?.prompt === 'notice' ? 'bar' : 'floating'} />
	);
}

ConsentBanner lists every variant and position.

To remove the notice card's radius, add these values to the existing options.components.banner slots. The root owns data-prompt, so the card uses Tailwind's group selector to read it:

const bannerSlots = {
	root: { className: 'group' },
	card: { className: 'group-data-[prompt=notice]:rounded-none' },
	rightLink: {
		className: 'underline-offset-4 data-[right=opt-out]:text-red-700',
	},
};

// In the existing options.components object:
// banner: bannerSlots

The root also carries data-variant, so you can restyle one shape without touching the others. Tailwind, giving a bar a brand-colored top edge and tighter text:

<ConsentProvider
	options={{
		mode,
		presentation: { prompt: { variant: 'bar' } },
		components: {
			banner: {
				root: { className: 'group' },
				card: {
					className:
						'group-data-[variant=bar]:border-t-4 group-data-[variant=bar]:border-t-emerald-600',
				},
			},
			description: {
				banner: { className: 'group-data-[variant=bar]:text-xs' },
			},
		},
	}}
/>

data-variant sits on the root, so child slots use Tailwind's group prefix to read it.

Class names and CSS-in-JS shows CSS Modules, vanilla-extract, StyleX and Emotion classes on a part.

Set noStyle on a component to drop the built-in classes from every part and keep only what your slots pass. Set noStyle in the provider options to do it for every component.

Component parts lists every part key under components and the data-* attributes each part carries. Action buttons carry data-action, so a single rule such as [data-action='dismiss'] styles one role across every surface. Right links are underlined text by default, so the only button on a notice is the primary action.

Component parts lists every component's part keys, including the ConsentGate placeholder's components['consent-gate'].root, .title and .button, and class names and CSS-in-JS covers CSS Modules, vanilla-extract, StyleX and Emotion.

Turn on dark mode and change motion

Pass colorScheme to ConsentTheme in your layout and the same value in options on ConsentRoot. Leave it unset to follow a dark class on <html>, such as the one next-themes sets, and add dark colors with theme.dark. Dark mode covers each value and a dark first paint.

options.disableAnimation on ConsentRoot turns off the banner and dialog animations, and the same prop on ConsentBanner or ConsentDialog overrides it for one surface. Motion and animation covers the duration and easing tokens and reduced motion.

Check the result

Open the page in a fresh session. The banner uses your colors and radius, and the primary action has the style you set. Choices, policy and actions are the same as with the default design; customizing never records a choice. The customization overview covers tokens, parts, copy and headless UI across frameworks.