---
title: Customize
description: Change the c15t banner and preference dialog on an Astro site with
  theme tokens, button styles, copy, dark mode, legal links and your own
  stylesheet order, all from the integration options.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Choose what to change

Astro takes every customization through the `c15t()` options in
`astro.config.mjs`, and renders it on the server. The options must survive JSON
serialization, so they hold values, not functions.

|Change|Use|
|--|--|
|Brand colors, radius, type, spacing|`theme` tokens|
|Which button looks primary|`theme.consentActions`|
|Wording on every page|`i18n.messages`|
|Wording on one banner|`ConsentBanner` props|
|Light or dark surfaces|`colorScheme` and `theme.dark`|
|One part of a component|`theme.slots`, or `class` on `ConsentBanner`|
|Animation|`disableAnimation`|
|Banner position and blocking|`presentation`|
|Links to your policies|`legalLinks`, and the banner's `legalLinks` prop|
|Your own CSS from scratch|`noStyle` on the banner, or `styles: false`|

[Customization](/docs/customization/overview) explains how
these choices relate 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.

## Set theme tokens

Put tokens in the integration's `theme` option:

```js title="astro.config.mjs (partial)"
c15t({
	theme: {
		colors: {
			primary: '#146b56',
			primaryHover: '#105442',
			textOnPrimary: '#ffffff',
		},
		radius: { lg: '1.25rem' },
	},
});
```

`ConsentScript` renders the tokens on the server as a
`<style id="c15t-theme">` element, so the banner and the dialog share them from
the first paint. Set hover and text colors together with a brand color, so each
button state stays readable. [Theme tokens](/docs/customization/tokens)
lists the tokens.

Put tokens in the integration options, not in the client entrypoint. The
browser does not turn tokens into CSS, so tokens in the client entrypoint's
`theme` have no effect.

## Style the banner buttons

`theme.consentActions` sets each button's `mode` and `variant`. c15t reads the
button's own key first, then `primary` for the action the policy marks as
primary, then `default`:

```js title="astro.config.mjs (partial)"
c15t({
	theme: {
		consentActions: {
			primary: { mode: 'filled' },
			customize: { mode: 'ghost', variant: 'neutral' },
		},
	},
});
```

The IAB TCF banner keeps its own button styles.

## Change the copy

For wording on every page and in the dialog, set `i18n.messages`, keyed by
language:

```js title="astro.config.mjs (partial)"
c15t({
	i18n: {
		messages: {
			en: {
				cookieBanner: {
					title: 'Cookies on this site',
					description:
						'We use cookies to measure traffic and show relevant ads.',
				},
			},
		},
	},
});
```

The server picks the visitor's language from `Accept-Language`. Set
`i18n.locale` to force one, or `detectLanguage: false` to use the default.

`i18n.messages` merges key by key with the copy for that language, so pass
only the keys you change. Setting `common.acceptAll` keeps every other label in
`common`. In `hosted()` and `manifest()` modes, the copy your backend sends is
the base, and your `i18n.messages` override it for the same language.
[Copy and translations](/docs/customization/translations) covers the message
groups.

For one banner, pass the `title`, `description`, `acceptButtonText`,
`rejectButtonText`, `customizeButtonText` or `dismissButtonText` props
instead. [Translations](/docs/frameworks/astro/translations) covers languages
and the message keys the banner reads.

## Change the banner's shape and position

`presentation.prompt` sets the banner's shape and where it sits:

```js title="astro.config.mjs (partial)"
c15t({ presentation: { prompt: { variant: 'bar', position: 'bottom' } } });
```

`variant` is `'floating'` (default), `'bar'`, `'widget'` or `'wall'`. The policy
still decides which actions the banner must offer, and a notice never becomes a
blocking wall. [Keep behavior and appearance separate](/docs/customization/overview#keep-behavior-and-appearance-separate)
explains what presentation can and cannot change.

## Set light or dark mode

The banner and dialog turn dark when `<html>` has the `c15t-dark` class. The
integration's `colorScheme` option decides who sets it:

|Value|Behavior|
|--|--|
|`'system'` (default)|Follows `prefers-color-scheme`, including changes while the page is open|
|`'dark'`|Always adds the class|
|`'light'`|Always removes the class|
|`'none'` or `null`|Never adds or removes the class. Your site sets it|

With the first three values, `ConsentScript` sets the class before first paint,
and c15t sets it again after each `ClientRouter` navigation. Set dark colors in
`theme.dark`. [Dark mode](/docs/customization/dark-mode) explains why Astro
follows the system setting by default.

Use `'none'` when your site has its own theme switch. Toggle `c15t-dark` on
`<html>` together with your own dark class. If your theme script does not run
on `astro:after-swap`, set the class again there, because a `ClientRouter`
swap replaces the attributes of `<html>`.

## Add links to your policies

Define the links once in the integration options:

```js title="astro.config.mjs (partial)"
c15t({
	legalLinks: {
		privacyPolicy: { href: '/privacy', label: 'Privacy policy' },
		cookiePolicy: { href: '/cookies', label: 'Cookie policy' },
	},
});
```

`ConsentBanner` and `ConsentDialog` show only the ones you list in their
`legalLinks` prop, such as `legalLinks={['privacyPolicy', 'cookiePolicy']}`.
A link without a `label` reads as the translated name for its type, such as
"Privacy Policy". See
[ConsentBanner legal links](/docs/frameworks/astro/components/consent-banner#legal-links).

## Style one part of a component

`theme.slots` in the integration options adds classes or inline styles to a
named part of the banner, the IAB banner and the dialogs:

```js title="astro.config.mjs (partial)"
c15t({
	theme: {
		slots: { consentBannerCard: 'brand-card', buttonPrimary: 'brand-button' },
	},
});
```

Define the classes in a global stylesheet. `class` on `ConsentBanner` goes on
the banner root. [Component parts](/docs/customization/slots) lists the slot
keys, and [Tailwind CSS](/docs/customization/tailwind) shows the Astro setup
for Tailwind 4 and 3.

## Turn off animations

`disableAnimation` in the integration options skips the banner's entry
animation and the dialogs' enter and exit animations. The same prop on
`ConsentBanner`, `ConsentDialog`, `IABConsentBanner` or `IABConsentDialog`
overrides it for one surface. c15t's stylesheet already stops these
animations for visitors who ask for reduced motion. [Motion and animation](/docs/customization/motion) covers the duration
and easing tokens.

## Load the stylesheet yourself

`ConsentScript`, or the first consent component on a layout without it,
inlines base and configured IAB banner rules once per page as `<style>`
elements. No c15t stylesheet holds back the first paint. When a visitor
first reaches for a dialog, c15t links its rules and waits for them before
mounting the surface. Svelte also loads its primitives stylesheet. The IAB
panel's rules load only when the IAB dialog opens.

On a Tailwind CSS 3 site, the integration adds `c15t/astro/styles.css` and,
with IAB configured, `c15t/astro/iab/styles.css` to every page instead, so
your `c15t/postcss-tailwind3` build processes them.
Tailwind 3 sites need no change, but their first paint still waits for that
stylesheet.
Do not import `c15t/astro/styles.css` as well. To control the cascade layer
order yourself, set `styles: false` and import the stylesheets from your own
CSS. [Stylesheets and CSS layers](/docs/customization/stylesheets#load-astros-stylesheets-yourself)
lists the files to import.

For markup with no c15t class names at all, pass `noStyle` to `ConsentBanner`
and style its `data-testid` and `data-*` attributes.

## Check your changes

* View the page source. It contains `<style id="c15t-theme">` with your tokens.
* Open the banner and the preference dialog in light and dark mode. Both use
  your colors, and every button label is readable.
* Change the browser language and reload. The copy follows it, or stays in
  `i18n.locale` when you set one.
* Navigate with `ClientRouter`. The colors and dark mode stay the same.
