---
title: Customize
description: Render c15t brand colors on the SvelteKit server with
  generateThemeCSS, and change slots, button styles, banner shape and copy.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Pick the right tool

|Change|Use|
|--|--|
|Brand colors, radius, type, spacing|`generateThemeCSS()` in a server load, or `--c15t-*` variables in a stylesheet|
|One part of one component|`theme.slots` on the provider|
|Button styles by action|`theme.consentActions`|
|Shape, position, blocking, button order|`ConsentBanner` props or `presentation`|
|Wording and languages|`ConsentBanner` text props, or [translations](/docs/frameworks/sveltekit/translations)|
|Different markup|[Headless](/docs/frameworks/sveltekit/headless)|

[Customization](/docs/customization/overview) explains how the pieces fit
together across frameworks.

See the [design gallery](/docs/customization/recipes) for five banner designs,
from a bottom bar to a fully custom one, with tested code for this framework.

## Render brand tokens on the server

The provider's `theme` prop does not turn colors, radius or other tokens into
CSS in the browser. Generate the CSS on the server instead, so the first paint
already uses your brand. Install `@c15t/ui`, which has the generator:

|Package manager|Command|
|:--|:--|
|npm|`npm install @c15t/ui@alpha`|
|pnpm|`pnpm add @c15t/ui@alpha`|
|yarn|`yarn add @c15t/ui@alpha`|
|bun|`bun add @c15t/ui@alpha`|

Put the theme in a module under `src/lib/server`. SvelteKit refuses to import
server-only modules into browser code, so the generator never reaches the bundle:

```ts title="src/lib/server/consent-theme.ts"
import { generateThemeCSS } from '@c15t/ui/theme';

// Modules in a `server` directory never reach the browser, so the theme
// generator stays on the server. The CSS is generated once, when the server
// starts.
export const themeCSS = generateThemeCSS({
	colors: {
		primary: '#6943a3',
		primaryHover: '#533285',
		textOnPrimary: '#ffffff',
	},
	radius: { lg: '18px' },
});
```

Return the CSS from a layout load. If your root `+layout.server.ts` already
exports `loadConsent` from the quickstart, call it from your own `load` and
return `themeCSS` next to its `consent`, as in
`({ ...(await loadConsent(event)), themeCSS })`. A separate layout looks like
this:

```ts title="src/routes/+layout.server.ts"
import { themeCSS } from '#lib/server/consent-theme.js';

import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = () => ({ themeCSS });
```

Render it in `<svelte:head>` in the layout:

```svelte title="src/routes/+layout.svelte"
<svelte:head>
	<!-- generateThemeCSS escapes `<`, so its output is safe in a style element. -->
	{@html `<style id="c15t-theme">${data.themeCSS}</style>`}
</svelte:head>
```

SvelteKit writes `<svelte:head>` content before its stylesheet links, and
`c15tHandle` adds c15t's default rules at the end of `<head>`. The
`generateThemeCSS` selectors are more specific than those defaults, so the
theme still wins. Keep the `c15t-theme` id. In development, the provider logs a warning when its
`theme` prop has tokens and the page has no `<style id="c15t-theme">`. `generateThemeCSS` escapes `<`,
so its output is safe inside a `<style>` element.

`generateThemeCSS` fills in what you leave out, such as a readable text color
for your primary color. Pass a color scheme as its second argument,
`'dark'` or `'system'`, to include dark tokens. Keep slot styles and
`consentActions` in the provider's `theme` prop; the server CSS covers tokens
only. [Theme tokens](/docs/customization/tokens) lists every token.

### Use a stylesheet instead

For fixed brand values you can skip the generator. Write the `--c15t-*`
variables on `:root` in a stylesheet and import it in the root layout:

```css title="src/consent-theme.css"
/* Load after @c15t/svelte/styles.css so these values win. */
:root {
	--c15t-primary: #6943a3;
	--c15t-primary-hover: #533285;
	--c15t-text-on-primary: #ffffff;
	--c15t-radius-lg: 18px;
}
```

Set the hover and text colors yourself; a stylesheet does not derive them.

Pick one of the two for each token. `generateThemeCSS` output outranks a plain
`:root` rule, so a `--c15t-*` value you set on `:root` loses to the generated
theme wherever the two overlap. To override a generated value from your
stylesheet, write the rule on `:root:root`, or move the value into the theme.

## Switch to dark tokens

The provider's `colorScheme` prop switches on the dark token set: `'dark'`
always, `'system'` when the operating system prefers dark, `'light'` never. It
toggles a `c15t-dark` class on `<html>`. Left unset, the provider copies a
`dark` class on `<html>` into `c15t-dark`, so an app with its own dark mode
toggle gets the dark tokens without the prop. Pass `null` if your app sets
`c15t-dark` itself. Override dark values under `:root.dark` and
`:root.c15t-dark` in your stylesheet. [Dark mode](/docs/customization/dark-mode)
covers a dark first paint.

## Style one part with theme slots

The provider's `theme` prop still applies slot styles in the browser. A slot
takes a class name, or `{ className, style, noStyle }`. Svelte applies a
slot's class and `style` on every stock part, including the IAB banner and
dialog:

```ts
const theme = {
	slots: {
		consentBannerCard: 'brand-banner-card',
		consentBannerTitle: 'brand-banner-title',
	},
};
```

Pass it as `theme={theme}` on `ConsentProvider`. Define the class in a
global stylesheet, or with `:global(.brand-banner-card)` in a component,
because Svelte scopes component styles. The Svelte components read these
slots:

|Component|Slots|
|--|--|
|`ConsentBanner`|`consentBanner`, `consentBannerCard`, `consentBannerHeader`, `consentBannerTitle`, `consentBannerDescription`, `consentBannerFooter`, `consentBannerFooterSubGroup`, `consentBannerRights`, `consentBannerRightLink`, `consentBannerTag`, `consentBannerOverlay`|
|`ConsentDialog`|`consentDialog`, `consentDialogCard`, `consentDialogHeader`, `consentDialogTitle`, `consentDialogDescription`, `consentDialogContent`, `consentDialogTag`|
|`ConsentWidget`|`consentWidget`, `consentWidgetFooter`, `consentWidgetFooterSubGroup`, `consentWidgetTag`|
|`ConsentDialogTrigger`|`consentDialogTrigger`, `consentDialogTriggerIcon`|
|`IABConsentBanner`|`iabConsentBanner`, `iabConsentBannerTag`, `iabConsentBannerOverlay`|
|`ConsentGate`|`consentGate`, `consentGateTitle`, `consentGateButton`|
|Buttons in every component|`buttonPrimary`, `buttonSecondary`|

Slot names are not React's `components.banner.card` keys; do not copy a React
configuration. Use slots when one part should change and tokens when a value
should change everywhere. `class` on a component, such as
`<ConsentBanner class="...">`, goes on its root.
[Component parts](/docs/customization/slots) explains the attributes each
element carries, and [Tailwind CSS](/docs/customization/tailwind) shows the
Svelte and SvelteKit setups for Tailwind 4 and 3.

## Change button styles by action

`theme.consentActions` picks each button's style by what it does, whichever
order the policy puts the buttons in:

```ts
const theme = {
	consentActions: {
		default: { variant: 'neutral', mode: 'stroke' },
		primary: { variant: 'primary', mode: 'filled' },
		reject: { variant: 'neutral', mode: 'filled' },
	},
};
```

A per-action entry overrides `primary`, which overrides `default`. The policy
decides which action is primary.

## Change the banner shape and position

Set the shape on the banner itself, or for every surface with the provider's
`presentation` prop:

```svelte
<ConsentBanner variant="bar" position="bottom" />
```

|`variant`|Positions|
|--|--|
|`floating`|`bottom-left` (default), `bottom-right`, `top-left`, `top-right`, `bottom-center`, `top-center`|
|`bar`|`bottom` (default), `top`|
|`widget`|`bottom-right` (default), `bottom-left`, `top-left`, `top-right`|
|`wall`|`center`|

`blocking` adds a backdrop, locks scrolling, traps focus and stops outside
clicks from closing the banner. `layout` and `primaryButton` change the button
order and emphasis. The policy wins where they conflict. A policy that requires
a reject button keeps it, whatever the layout says.

## Change the copy

For one banner, the text props on `ConsentBanner`, such as `title` and
`rejectButtonText`, replace the copy in every mode:

```svelte
<ConsentBanner title="Your privacy" rejectButtonText="Reject optional" />
```

For site-wide copy, pass the provider's `i18n` messages. In `offline()` mode
they merge over the bundled English copy. In `hosted()` mode they merge over
the backend's copy for the same language, key by key, so keys you leave out
keep the wording from your Inth project.
[Translations](./translations) covers both, the message keys and switching
languages. [Copy and translations](/docs/customization/translations) covers
wording for each action.

## Turn off animations

`disableAnimation` on the provider turns off the banner and dialog animations,
and the same prop on `ConsentBanner`, `ConsentDialog`, `IABConsentBanner` or
`IABConsentDialog` overrides it for one surface. Left unset, the provider
follows the visitor's reduced motion setting.
[Motion and animation](/docs/customization/motion) covers the duration and
easing tokens.

## Remove c15t's styles

Set `noStyle` on one component or on the provider to render the same markup
and behavior without c15t's classes. Style the `data-*` attributes and your
slot classes instead. For different markup, see the headless page.
