Skip to main content

JavaScript Customization

Customize

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:

ChangeOption
Colors, radius, fonts, spacingui.theme
One part's CSSui.theme.slots with ui.stylesheetURLs, ::part(), or ui.css
Your own stylesheetui: { shadow: false }
Banner shape, position, button layout, blockingpresentation.prompt
Banner textui.banner
Wording and languages everywherei18n; see translations
Legal linkslegalLinks with ui.banner.legalLinks and ui.dialog.legalLinks
A reopen buttonui.trigger
Entirely different markupHeadless

The design gallery 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:

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. 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 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:

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 and Tailwind CSS show both, and component parts 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:

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:

presentation: {
  prompt: { variant: 'bar', position: 'bottom', uiProfile: 'balanced' },
},
variantPositions
floating (default)bottom-left (default), bottom-right, top-left, top-right, bottom-center, top-center
barbottom (default), top
widgetbottom-right (default), bottom-left, top-left, top-right
wallcenter

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

These go under ui.banner.

OptionTypeDefaultWhat it does
titlestringtranslated titleHeading. A notice uses the notice title by default.
descriptionstringtranslated descriptionBody text. A notice uses the notice description by default.
acceptButtonTextstring"Accept All"Label of the accept button.
rejectButtonTextstring"Reject All"Label of the reject button.
customizeButtonTextstring"Customize"Label of the button that opens the dialog.
legalLinkslist of privacyPolicy, cookiePolicy, termsOfService, or nullnoneWhich configured legal links to show after the description. null or [] shows none.
hideBrandingbooleanfalseHide the "Secured by" tag. The IAB banner always keeps it.
scrollLockbooleanfrom presentationDeprecated. Use presentation.prompt.blocking.
trapFocusbooleanfrom presentationDeprecated. 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.

Set the links once and list them per surface:

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 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:

OptionTypeDefaultWhat 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.
ariaLabelstring"Open privacy settings"The button's accessible name. It has no visible text.
persistPositionbooleantrueRemember 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:

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.

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.