Skip to main content

Customization

Stylesheets and CSS layers

What each framework loads

FrameworkWhat you importHow the styles arrive
Next.js, TanStack Start, ReactNothingEach stock surface renders the rules it uses as <style> elements
Nuxt, VueNothingEach Vue component imports its own stylesheet
AstroNothing<ConsentScript /> inlines the banner's rules into the HTML. The client links the dialog's rules when a dialog first opens. On a Tailwind CSS 3 site, the integration adds c15t/astro/styles.css to every page instead
SvelteNothingEach stock surface inserts the rules it uses into <head>
SvelteKitNothingc15tHandle writes a server-rendered banner's rules into the HTML. The other surfaces insert theirs in the browser
HTMLNothingc15t.js carries its stylesheet into the UI's shadow root
JavaScriptNothinginit() carries its stylesheet into the UI's shadow root

Inline c15t styles avoid a stylesheet request before the page's first paint. A linked stylesheet in <head> used to delay it by a round trip. With 4x CPU throttling, 1.6 Mbps download and 150 ms RTT, a Next.js page first painted at about 370 ms instead of 650 ms once the link was gone.

Inline rules add bytes to each server-rendered HTML response. Enable HTTP compression on the production host to reduce their transfer cost. On repeat document visits, a cached external stylesheet can paint sooner than sending the inline rules again. To use that cache, keep the stylesheet imports and set styles: false as described below.

Load styles.css yourself with Tailwind CSS 3 or a named cascade layer. Import the stylesheet yourself covers those cases.

How the surfaces deliver their rules

c15t splits its rules into two sheets. The first-paint sheet holds the default tokens, every c15t CSS variable, and the rules for the banner, the floating trigger and the ConsentGate placeholder. The dialog sheet holds the preference dialog's and the preference widget's rules.

In React, Next.js and TanStack Start, ConsentBanner, ConsentDialogTrigger and the default ConsentGate placeholder render the first-paint sheet. ConsentDialog and ConsentWidget render it plus the dialog sheet. The dialog sheet ships with the dialog's lazily loaded code, so it is not part of the first page load. A server-rendered banner puts its rules in the HTML.

IABConsentBanner also delivers the IAB variables and banner rules. IABConsentDialog adds the IAB dialog rules when it opens, including the shared rules it needs when rendered without a banner. IAB setups need no stylesheet imports with automatic styling enabled.

Streamed React IAB banners become visible once their complete card markup arrives. This prevents the centered card from moving as the browser parses its remaining content, without waiting for hydration.

  • In React 19, React moves the elements into <head> and renders each sheet once per page, as <style data-precedence="c15t" data-href="c15t-first-paint">.
  • In React 18, the elements render in place, next to the surface.
  • In React 19 with the provider's nonce option set, they also render in place and carry the nonce. React drops the nonce of a style it moves to <head> unless the app passed that nonce to React's server renderer.

In Svelte, each stock surface inserts the sheets it uses into <head> as <style data-c15t-styles="…">, once per page, before its own elements render. The elements stay after the surface unmounts. The Svelte dialog's sheets, its rules and the primitives, load with the dialog's code.

On a server-rendered SvelteKit page, the banner leaves a <meta name="c15t-styles"> marker, and c15tHandle from @c15t/svelte/kit writes the sheets into the HTML <head> as <style> elements. The banner is styled before hydration with no stylesheet request. Without c15tHandle in src/hooks.server.ts, a server-rendered banner shows unstyled until hydration. The SvelteKit quickstart adds the handle.

In Astro, <ConsentScript />, or the banner on a layout without it, inlines the first-paint sheet into the HTML as <style data-c15t-styles="c15t-first-paint"> with the page nonce. When tailwindcss resolves to version 3 from the Astro root, the integration inlines nothing and adds c15t/astro/styles.css to every page through Astro's CSS pipeline, so the site's c15t/postcss-tailwind3 plugin processes it. That stylesheet still holds back the first paint.

The surfaces render no stylesheet with noStyle, or when the provider sets styles: false.

Import the stylesheet yourself

Set styles: false in the provider options and import styles.css when you need control over the file:

  • Tailwind CSS 3. c15t's <style> rules sit in a native @layer components, and Tailwind 3's unlayered preflight beats them. Import styles.css through the postcss-tailwind3 plugin instead, as Tailwind CSS shows. Astro sites skip this: the integration detects Tailwind 3 and links the stylesheet itself.
  • A named cascade layer. Import the stylesheet into your own layer, as in order c15t's layer against your CSS.
  • Cached styles across document visits. Keep external CSS when its HTTP cache performs better for your site's repeat visits. Compare cold and warm visits on your production host before choosing this option.
FrameworkOptionFiles to import
Next.jsoptions.styles in c15t.config.ts or on ConsentRootc15t/next/styles.css, and c15t/next/iab/styles.css for IAB TCF
TanStack Startoptions.styles on ConsentRootc15t/tanstack-start/styles.css, and c15t/tanstack-start/iab/styles.css for IAB TCF
Reactoptions.styles on ConsentProviderc15t/react/styles.css, and c15t/react/iab/styles.css for IAB TCF
Svelte, SvelteKitoptions.styles on ConsentProvider or ConsentRoot@c15t/svelte/styles.css, and @c15t/svelte/iab/styles.css for IAB TCF
Astrostyles in the c15t() integration optionsSee load Astro's stylesheets yourself
c15t.config.ts (Next.js)
import { defineConsentConfig } from 'c15t/next';

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

styles: false turns off only the stylesheets. Tokens from ConsentTheme and slot classes still apply.

If you keep a styles.css import without styles: false, the surfaces still look right, but the rules ship twice and the stylesheet link still holds back the first paint. Remove the import, or add styles: false if you need it.

Where the dialog's rules come from

FrameworkThe preference dialog's rules
Next.js, TanStack Start, ReactIn the dialog sheet that ConsentDialog and ConsentWidget render. It ships with the dialog's lazily loaded code. With styles: false, in styles.css. No @c15t/react or @c15t/ui module imports CSS, so a Next.js app that uses only the Pages Router builds without transpilePackages
Nuxt, VueThe dialog component imports its stylesheet, so the rules load with the dialog's code, not the banner's
AstroIn @c15t/ui/styles/sheets/dialog.css. The Svelte dialog, the default ui, also needs the primitives stylesheet. c15t links both when a visitor first points at, focuses or opens a control that opens the dialog, waits for them before it shows the dialog, and keeps them across ClientRouter navigation. On a Tailwind CSS 3 site, the dialog's rules are in c15t/astro/styles.css, and c15t links only the primitives stylesheet
Svelte, SvelteKitIn the dialog's sheets, which load with the dialog's code. With styles: false, in @c15t/svelte/styles.css
HTML, JavaScriptPart of the stylesheet in the shadow root, which holds only the rules the stock surfaces use

styles.css still holds every stock surface's rules for apps that set styles: false. In that manual mode, IAB TCF also needs iab/styles.css after the base stylesheet.

Earlier 3.0 alphas shipped the dialog's rules in @c15t/ui/styles/dialog.css, which the dialog's code imported. That file, the @c15t/ui/styles/dialog module and c15t/astro/dialog.css are now empty and stay only so existing imports keep resolving. Remove those imports.

Order c15t's layer against your CSS

c15t puts its component rules in the cascade layer components. Tokens, variables and keyframes stay unlayered. Each stylesheet, and each <style> element the surfaces render, opens with Tailwind CSS 4's layer order:

@layer properties, theme, base, components, utilities;

Cascade layers rank by the order they are first named, so components stays above Tailwind's preflight in base and below utilities, whichever stylesheet loads first. Without Tailwind the other layers stay empty. Vue's per-component stylesheets open with the same statement.

What this means for your overrides:

  • Any unlayered rule of yours outranks every c15t component rule, whatever its specificity or load order. You do not need !important.
  • A rule in one of your own layers wins only if that layer comes after components in the layer order.
  • Tokens are not layered. Set them as described in theme tokens.

If your CSS declares its own layer order, load that statement before c15t's rules. To put c15t's rules in a named layer, set styles: false and import the stylesheet into that layer, such as @import 'c15t/react/styles.css' layer(c15t). Its component rules then sit in c15t.components, so order c15t against your own layers.

Load Astro's stylesheets yourself

By default the Astro integration inlines the banner's rules into each page, including the IAB banner rules when it sets iab, and links the dialog's rules when a dialog opens. On a Tailwind CSS 3 site it adds c15t/astro/styles.css and, with IAB, c15t/astro/iab/styles.css to every page instead of inlining the banner's rules. To load the files yourself, for example from a global stylesheet that names its own layers, set styles: false in the c15t() options and import them after your layer order statement. With styles: false, c15t inlines no rules and links no stylesheet when the dialog opens. The Svelte dialog, the default ui, needs the primitives stylesheet next to styles.css:

src/styles/global.css
@import 'c15t/astro/styles.css';
/* Only with ui: 'svelte'. */
@import 'c15t/astro/primitives.css';
/* Only when the integration sets iab. */
@import 'c15t/astro/iab/styles.css';

Keep c15t's rules when you restyle the surfaces. The browser hides the Astro banner with the hidden attribute, and c15t's rules make that attribute beat the banner's own display rule.

Style the script tag's shadow root

The script tag and init() from @c15t/browser render into a shadow root with their own copy of the stylesheet. Your page's CSS does not reach the UI, and the UI's CSS does not reach your page. To style it:

OptionWhat it does
ui.themeTokens, written after the stylesheet inside the shadow root
ui.cssA string of CSS added after the stylesheet and the theme
ui.stylesheetURLsStylesheets linked inside the shadow root, after c15t's, with the client's nonce. They load asynchronously, so the first frame can render without them
::part()Page CSS that reaches a part by its slot key, as in [data-c15t-ui]::part(consentBannerCard)
ui.shadow: false, or data-shadow="false"Renders into the page, so your stylesheets apply. c15t still injects its stylesheet next to the UI
ui.styles: falseDrops the injected stylesheet. With shadow: false, load @c15t/browser/styles.css from your bundle, or link dist/c15t.css from the same package version as the script

A stylesheet in ui.stylesheetURLs should also load on the page. Some CSS, such as Tailwind 4's @property rules, only takes effect in the page's stylesheets. Of these options, the script tag has an attribute for shadow only. Set the others with c15t.push(['config', { ui: { ... } }]).

Run without c15t's styles

noStyle removes c15t's stock classes from the markup, and in the script tag also the injected stylesheet. Your part classes, data-testid and the data-* attributes stay. Component parts lists where each framework takes it.

noStyle does not supply layout, spacing, focus indicators or responsive behavior. If a token seems to do nothing, check the layer order and your selector before you reach for noStyle.

Check the result

  1. Open DevTools Network, clear site data and reload. No c15t stylesheet request appears before the first paint. In the HTML or the Elements panel, <head> holds c15t's rules: <style data-href="c15t-first-paint"> in React 19, or a <style data-c15t-styles> element in Svelte, SvelteKit and Astro. In React 18, or with a nonce, the <style> sits next to the banner instead.
  2. Open the preference dialog. It is styled on its first frame. Opening it adds the dialog's rules: a second <style> element in React, Next.js, TanStack Start, Svelte and SvelteKit, or a linked @c15t/ui/styles/sheets/dialog.css in Astro.
  3. With styles: false, your imported styles.css loads once, and no c15t <style> element appears.
  4. Select a banner part and read the Styles panel. c15t's rules appear under @layer components, and your unlayered rules for the same property win.