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:
| 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 |
| Legal links | legalLinks with ui.banner.legalLinks and ui.dialog.legalLinks |
| A reopen button | ui.trigger |
| Entirely different markup | Headless |
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:
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:
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:
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:
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:
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:
| 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:
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
- Run the app and open it in a private window. The banner uses your tokens and layout.
- Open the dialog. It uses the same tokens and shows your legal links.
- Switch your system between light and dark mode. With
colorScheme: 'system', the UI follows.