Skip to main content

Customization

Tailwind CSS

How c15t's styles meet Tailwind

c15t's component rules live in the CSS cascade layer components. Each c15t stylesheet starts with Tailwind CSS 4's layer order, @layer properties, theme, base, components, utilities;, and cascade layers rank by the order they are first named. On Tailwind 4, c15t's rules stay above Tailwind's preflight in base and below your utilities in utilities, whichever stylesheet loads first. A plain utility such as p-2 on a c15t part wins without !important.

Tailwind CSS 3 has no cascade layers. It treats @layer components as its own directive and fails the build on a stylesheet that uses it without a matching @tailwind components. c15t's Tailwind 3 PostCSS plugin removes the layer wrappers from c15t's stylesheets before Tailwind 3 runs. c15t's rules then win by specificity, as they did in c15t v2, so a utility on a c15t part needs Tailwind's important modifier, such as !p-2. The plugin cannot reach the <style> elements c15t renders itself, so on Tailwind 3 you turn those off with styles: false and import the stylesheet instead. The Astro integration does this for you when it finds Tailwind 3.

The setups below come from fixtures that CI builds for each framework and checks in Chromium. The Next.js snippets come from the Next.js 16 Turbopack fixture. The Next.js 15 and webpack fixtures use the same files. The check confirms that c15t's banner and dialog keep their styles under Tailwind's preflight, that a padding utility on the banner root beats c15t's own padding, and that the dark: variant applies. The fixtures run without a backend, so they use offline() mode or a local backendURL. Keep the mode or backendURL from your quickstart.

Set up Tailwind CSS 4

Install Tailwind's plugin for your build tool and add it to the build. c15t loads its own styles in every framework, so your CSS entry holds only Tailwind. Do not import c15t's stylesheet next to Tailwind 4: the stock components already render the same rules, and the import would hold back the first paint.

Next.js runs Tailwind 4 through @tailwindcss/postcss:

export default { plugins: { '@tailwindcss/postcss': {} } };

Import Tailwind in the global stylesheet that app/layout.tsx or pages/_app.tsx loads:

@import 'tailwindcss';

@custom-variant dark (&:where(.dark, .dark *));

Set up Tailwind CSS 3

Tailwind 3's preflight is unlayered, so it beats the layered rules c15t renders itself and strips the banner's padding and borders. On Tailwind 3, turn those rules off with styles: false and import c15t's styles.css through a PostCSS plugin. The Astro integration needs only the plugin: it detects Tailwind 3 and links c15t/astro/styles.css itself. Every c15t package that ships a stylesheet exports the plugin as <package>/postcss-tailwind3, so use the one from the package you installed:

You installedPlugin name
c15t (React, Next.js, TanStack Start, Vue, Nuxt, Astro)c15t/postcss-tailwind3
@c15t/svelte (Svelte, SvelteKit)@c15t/svelte/postcss-tailwind3
@c15t/browser (script tag in the light DOM)@c15t/browser/postcss-tailwind3

They are the same plugin. A config that already lists @c15t/ui/postcss-tailwind3 keeps working.

Make four changes:

  1. Set styles: false where you configure c15t: options in Next.js c15t.config.ts, options on ConsentRoot in TanStack Start, options on ConsentProvider in React, and styles={false} on ConsentProvider in Svelte or ConsentRoot in SvelteKit. Vue, Nuxt and Astro need no change.
  2. Add the plugin to your PostCSS plugins, before tailwindcss. Write the plugins as an object, as the examples below do. PostCSS runs them in the order of the keys. Next.js also accepts an array of plugin names, but Vite's PostCSS loader rejects that form.
  3. Import c15t's stylesheet above the @tailwind directives, whatever your build tool. Vue, Nuxt and Astro skip this step. Vite drops an @import that follows another rule, with the warning @import must precede all other statements, and the banner renders unstyled. Next.js 16 with Turbopack fails the build.
  4. Set darkMode: 'class' so Tailwind's dark: variant follows the same dark class c15t reads.

The plugin also processes the stylesheets the Vue and Nuxt components import, and the one the Astro integration adds on a Tailwind 3 site. Without it, Tailwind 3 stops the build with "@layer components is used but no matching @tailwind components directive is present", or purges c15t's rules, because c15t's class names never appear in your source.

Tailwind 3 has no cascade layers, so c15t's selectors outrank a single utility class. Add the important modifier to utilities you pass to c15t parts: !bg-red-500, hover:!bg-red-500. A bare bg-red-500 has no effect. Theme tokens work the same on Tailwind 3 and 4.

In React and Next.js apps, c15t setup adds the plugin and the stylesheet import when it finds Tailwind 3, but does not set styles: false. Add that yourself. It replaces an existing styles.tw3.css import from an earlier setup with styles.css. When it cannot edit your PostCSS config, it prints the step to do by hand. See setup.

Create React App ignores PostCSS config files, so Tailwind 3 cannot run the plugin and the build fails on c15t's stylesheet. c15t setup warns about this. Add the plugin before tailwindcss through CRACO, eject, or move the app to Vite.

Next.js loads postcss.config.mjs itself:

export default {
	plugins: { 'c15t/postcss-tailwind3': {}, tailwindcss: {}, autoprefixer: {} },
};

Scan app/ and turn on the class-based dark variant:

import type { Config } from 'tailwindcss';

export default {
	content: ['./app/**/*.{ts,tsx}'],
	darkMode: 'class',
} satisfies Config;

Import c15t/next/styles.css above the directives. Turbopack fails the build when the import sits below them. The same order works with webpack. Set styles: false in ConsentRoot's options as well:

/* Turbopack rejects an @import below other rules, so it goes first. */
@import 'c15t/next/styles.css';

@tailwind base;
@tailwind components;
@tailwind utilities;

Put utilities on c15t parts

Pass utilities to a component part through your framework's part API. The fixtures put padding on the banner root, which c15t also pads, with a different value in dark mode. On Tailwind 4 a plain utility wins. On Tailwind 3, add !, as in !p-[7px] dark:!p-[11px], because c15t's unlayered rules outrank a plain utility. Component parts lists the parts each framework exposes.

Tailwind only generates classes it finds in the files its content globs or @source rules scan. Keep part classes in a scanned file.

Add the classes to options.components on ConsentRoot, in the 'use client' wrapper from your quickstart:

options={{
	components: {
		banner: {
			root: { className: 'p-[7px] dark:p-[11px]' },
		},
	},
	mode,
}}

Use the dark variant

The fixtures make Tailwind's dark: variant follow a dark class on <html>: @custom-variant dark (&:where(.dark, .dark *)); on Tailwind 4 and darkMode: 'class' on Tailwind 3. c15t's stylesheet reads the same class, and in React, Next.js, TanStack Start, Vue, Nuxt, Svelte and SvelteKit an unset colorScheme also copies it into c15t-dark. One toggle then switches your utilities and c15t's dark tokens together.

Astro and the script tag follow the system setting by default. Dark mode covers each case.

Render the UI in the light DOM

The script tag and init() render into a shadow root, so your page's CSS does not reach c15t and c15t's CSS does not reach your page. To style the UI with your page's Tailwind build directly, render it in the page with data-shadow="false" or ui: { shadow: false }.

On Tailwind 4, that is the whole change. c15t still injects its layered stylesheet, and the layer order keeps it above preflight:

light-dom.html
<script
	src="/c15t.offline.js"
	data-mode="offline"
	data-country="DE"
	data-shadow="false"
	defer
></script>

On Tailwind 3, keep the default shadow root. Tailwind 3's preflight is unlayered, so in the page it beats c15t's layered rules whatever their specificity and removes the button padding and card borders. Linking c15t.css from the page does not help, because nothing removes its layers.

To render in the light DOM anyway, turn the injected sheet off with styles: false, and build c15t's rules into your Tailwind CSS with the plugin instead:

light-dom.html
<script>
	window.c15t = [['config', { ui: { shadow: false, styles: false } }]];
</script>
<script
	src="/c15t.offline.js"
	data-mode="offline"
	data-country="DE"
	defer
></script>

Import @c15t/browser/styles.css above the directives in your Tailwind entry. Install @c15t/browser@alpha, which provides both the stylesheet and @c15t/browser/postcss-tailwind3:

style.css
/*
 * With `shadow: false`, c15t's injected stylesheet is layered and Tailwind
 * 3's preflight is not, so preflight wins. Turn the injected sheet off and
 * build c15t's rules into your Tailwind CSS instead.
 */
@import '@c15t/browser/styles.css';

@tailwind base;
@tailwind components;
@tailwind utilities;
postcss.config.mjs
export default {
	plugins: {
		'@c15t/browser/postcss-tailwind3': {},
		tailwindcss: {},
		autoprefixer: {},
	},
};

In the light DOM your page's global rules reach the banner too, so check buttons and headings after the switch.

Check the result

  1. Build the app and open it in a private window. The banner has its card background, border and button padding, so preflight did not reset it.
  2. Inspect the part you gave a utility. In the Styles panel, your utility is applied and c15t's rule for the same property is struck out.
  3. Add the dark class to <html>. Your dark: utility applies and, where c15t follows the class, the banner switches to dark colors.
  4. Open the preference dialog. Its rules come from a separate stylesheet in most frameworks, so check that it is styled too.