---
title: Customize
description: Change the stock @c15t/browser banner, preference dialog and
  floating trigger in a bundled JavaScript app with theme tokens, CSS, layout,
  copy, legal links and UI options.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Pick the right option

The stock UI from `init()` in `@c15t/browser` is a banner, a preference
dialog and an optional floating trigger. Each change has one place to go:

|Change|Option|
|--|--|
|Colors, radius, fonts, spacing|`ui.theme`|
|One part's CSS|`ui.theme.slots` with `ui.stylesheetURLs`, `::part()`, or `ui.css`|
|Your own stylesheet|`ui: { shadow: false }`|
|Banner shape, position, button layout, blocking|`presentation.prompt`|
|Banner text|`ui.banner`|
|Wording and languages everywhere|`i18n`; see [translations](/docs/frameworks/javascript/translations)|
|Legal links|`legalLinks` with `ui.banner.legalLinks` and `ui.dialog.legalLinks`|
|A reopen button|`ui.trigger`|
|Entirely different markup|[Headless](/docs/frameworks/javascript/headless)|

The [design gallery](/docs/customization/recipes) shows banner designs built
from these options.

## Change colors, radius and fonts

Pass tokens in `ui.theme`. This version of `init()` turns the buttons purple
and rounds the cards:

```ts title="src/main.ts"
const consent = init({
	mode: hosted({ backendURL: 'https://your-project.inth.app' }),
	scripts,
	ui: {
		theme: {
			colors: {
				primary: '#6943a3',
				primaryHover: '#533285',
				textOnPrimary: '#ffffff',
			},
			radius: { lg: '18px' },
		},
	},
});
```

`theme` takes the same token object as every c15t package; see
[theme tokens](/docs/customization/tokens). The banner, the dialog and the
trigger all read it. `ui.colorScheme` picks `'light'`, `'dark'` or
`'system'`, the default, which follows the visitor's setting. `null` follows a
`dark` or `c15t-dark` class on your page's `<html>` instead.
[Dark mode](/docs/customization/dark-mode) covers dark tokens in
`ui.theme.dark`.

## Add CSS inside the shadow root

The UI renders in a shadow root with its own copy of the stylesheet, so your
app's CSS does not reach it and its CSS does not reach your app. To target
one part, pass CSS in `ui.css`. c15t adds it after the bundled stylesheet:

```ts
ui: {
  css: '[data-testid="consent-banner-card"] { border: 3px solid #6943a3; }',
},
```

Every part has a `data-testid`, such as `consent-banner-card`,
`consent-banner-accept-button`, `consent-dialog-root`,
`consent-widget-switch-measurement` and `consent-dialog-trigger`. Inspect the
UI in the browser's element panel to find one.

To style parts with classes from your own stylesheet, such as a Tailwind or
CSS Modules build, put the classes on parts with `ui.theme.slots` and link the
stylesheet into the shadow root with `ui.stylesheetURLs`. Page CSS can also
reach a part by its slot key with `[data-c15t-ui]::part(consentBannerCard)`.
[Class names and CSS-in-JS](/docs/customization/class-names) and
[Tailwind CSS](/docs/customization/tailwind) show both, and
[component parts](/docs/customization/slots) lists the slot keys.

## Use your own stylesheet

`ui: { shadow: false }` renders into the page, so your stylesheet applies.
c15t still adds its own stylesheet. To load it from your bundle instead,
import it and turn the inline copy off:

```ts
import '@c15t/browser/styles.css';

const consent = init({
	mode: manifest(),
	ui: { shadow: false, styles: false },
});
```

If your bundler runs Tailwind 3, add `@c15t/browser/postcss-tailwind3` before
`tailwindcss` in your PostCSS plugins, using the object form:
`{ '@c15t/browser/postcss-tailwind3': {}, tailwindcss: {} }`. Without the
plugin, Tailwind 3 drops the imported rules without an
error and its preflight overrides the UI. A page that keeps the default shadow
root needs none of this, because page CSS does not reach the UI.

`ui: { shadow: false, noStyle: true }` renders markup with no c15t classes,
for a design written from scratch. Parts keep their `data-testid`. Your app's
global rules reach the UI in either mode, so check buttons and headings after
you switch.

## Change the banner's layout

`presentation.prompt` sets the banner's shape, position and button layout:

```ts
presentation: {
  prompt: { variant: 'bar', position: 'bottom', uiProfile: 'balanced' },
},
```

|`variant`|Positions|
|--|--|
|`floating` (default)|`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`|

`layout`, `primaryActions`, `direction`, `uiProfile` and `blocking` arrange
the buttons and decide whether the banner blocks the page.
`presentation.preferences` does the same for the dialog and adds `defaults`,
the starting state of switches the visitor has not set. The policy still
wins. A notice never blocks, a wall always does, and required buttons come
back if a layout drops them.

## Change the banner text

## Banner options

These go under `ui.banner`.

|Option|Type|Default|What it does|
|--|--|--|--|
|`title`|`string`|translated title|Heading. A notice uses the notice title by default.|
|`description`|`string`|translated description|Body text. A notice uses the notice description by default.|
|`acceptButtonText`|`string`|"Accept All"|Label of the accept button.|
|`rejectButtonText`|`string`|"Reject All"|Label of the reject button.|
|`customizeButtonText`|`string`|"Customize"|Label of the button that opens the dialog.|
|`legalLinks`|list of `privacyPolicy`, `cookiePolicy`, `termsOfService`, or `null`|none|Which configured legal links to show after the description. `null` or `[]` shows none.|
|`hideBranding`|`boolean`|`false`|Hide the "Secured by" tag. The IAB banner always keeps it.|
|`scrollLock`|`boolean`|from `presentation`|Deprecated. Use `presentation.prompt.blocking`.|
|`trapFocus`|`boolean`|from `presentation`|Deprecated. Use `presentation.prompt.blocking`.|

The dismiss button of a notice always uses the translated
`common.acknowledge` label. Change it through `i18n`.

The dialog's copy comes from translations only.

## Show legal links

Set the links once and list them per surface:

```ts
legalLinks: {
  privacyPolicy: { href: '/privacy' },
  cookiePolicy: { href: '/cookies' },
},
ui: {
  banner: { legalLinks: ['privacyPolicy', 'cookiePolicy'] },
  dialog: { legalLinks: ['privacyPolicy'] },
},
```

Each link opens in a new tab unless its `target` says otherwise. `label`
replaces the translated link text.

## Add the floating trigger

`ui: { trigger: true }` shows a corner button that opens the dialog while no
other surface is open. Visitors can drag it to another corner, and c15t
remembers that corner. While the [DevTools panel](/docs/frameworks/javascript/dev-tools)
is mounted, the trigger also carries a button that opens it.

## Trigger options

`ui.trigger: true` shows the trigger with its defaults. An object sets these:

|Option|Type|Default|What it does|
|--|--|--|--|
|`position`|`'bottom-right'`, `'bottom-left'`, `'top-right'` or `'top-left'`|`'bottom-right'`|The corner the button starts in.|
|`size`|`'sm'`, `'md'` or `'lg'`|`'md'`|Button size.|
|`showWhen`|`'always'` or `'after-consent'`|`'always'`|`after-consent` hides the button until the visitor has answered the prompt.|
|`ariaLabel`|`string`|"Open privacy settings"|The button's accessible name. It has no visible text.|
|`persistPosition`|`boolean`|`true`|Remember the corner a visitor dragged the button to, in localStorage. A remembered corner wins over `position`.|

## Mount the UI somewhere else

`ui.container` takes an element or a selector for the UI host, instead of
`<body>`. `consent.mountUI(options)` removes the UI and mounts it again with
new options, for example after your app changes theme:

```ts
consent.mountUI({ colorScheme: 'dark', theme });
```

The options you pass replace the configured `ui` options, so include every
option you still want, such as `theme` here.

## Keyboard and screen readers

The stock UI handles focus for you. A blocking banner and the dialog keep
Tab inside themselves and stop the page scrolling. Escape closes the dialog
without saving, wherever focus is, including a non-blocking dialog. The
dialog has no close button by design. Surfaces set `lang` and `dir` from the resolved language.
Transitions follow the visitor's reduced motion setting, and
`ui.disableAnimation` turns them off. `ui.banner.disableAnimation` and
`ui.dialog.disableAnimation` set it for one surface; see
[motion and animation](/docs/customization/motion).

## Check it works

1. Run the app and open it in a private window. The banner uses your
   tokens and layout.
2. Open the dialog. It uses the same tokens and shows your legal links.
3. Switch your system between light and dark mode. With `colorScheme: 'system'`, the UI follows.
