Skip to main content

Customization

Component parts

Which part API your framework uses

Each framework has one way to add classes, inline styles or attributes to a single part of a stock component. The keys differ between the two vocabularies, so a configuration from one does not work in the other.

FrameworkPart APIKeys look likeClass key
Next.js, TanStack Startoptions.components in Next.js c15t.config.ts, or on ConsentRootcomponents.banner.cardclassName
Reactcomponents in the ConsentProvider optionscomponents.banner.cardclassName
Nuxtcomponents in the c15t options, or in app/app.config.tscomponents.banner.cardclass
Vuecomponents in the c15tVue optionscomponents.banner.cardclass
Astrotheme.slots in the c15t() integration optionsslots.consentBannerCardA string, or className
Svelte, SvelteKittheme.slots on ConsentProvider (Svelte) or ConsentRoot (SvelteKit)slots.consentBannerCardA string, or className
HTML, JavaScriptui.theme.slotsslots.consentBannerCardA string, or className

React and Vue also read theme.slots from the theme in their options, and apply each slot to the matching components part, such as consentDialogCard to dialog.card and toggle to switch.root. Where both set the same part, classes from both apply, inline styles merge with components winning per property, and any other attribute in components replaces the theme's. In Vue and Nuxt, each part object is bound with v-bind, so write class and style rather than React's className.

Svelte's stock components, and Astro's ConsentBanner, IABConsentBanner and ConsentDialogTrigger, also take a class prop. On ConsentBanner it goes on the banner root.

This React fragment belongs in the provider options:

components: {
  banner: {
    card: { className: 'brand-card' },
    title: { style: { fontWeight: 600 } },
  },
},

The same parts with theme.slots, which every framework reads:

theme: {
  slots: {
    consentBannerCard: 'brand-card',
    consentBannerTitle: 'brand-title',
  },
},

A slot can also be { className, style }. Svelte applies both on every part, including the IAB banner and dialog.

Class names and CSS-in-JS covers CSS Modules, vanilla-extract, StyleX and Emotion, and Tailwind CSS covers utilities on parts.

Partcomponents keytheme.slots keydata-testid
Positioning root, with the state attributesbanner.rootconsentBannerconsent-banner-root
Card sizing and branding containerbanner.cardShellNone
Visible cardbanner.cardconsentBannerCardconsent-banner-card
Title and description containerbanner.headerconsentBannerHeader
Titlebanner.titleconsentBannerTitle
Descriptiondescription.bannerconsentBannerDescription
Action areabanner.footerconsentBannerFooter
Group of action groupsbanner.actionsNone
One group of equally prominent actionsbanner.actionGroupconsentBannerFooterSubGroup
Group of right linksbanner.rightsconsentBannerRights
One right linkbanner.rightLinkconsentBannerRightLink
Backdrop of a blocking bannerbanner.overlayconsentBannerOverlay
"Secured by" tagtag.bannerconsentBannerTagconsent-banner-branding
Filled and outlined buttonsbutton.primary, button.secondarybuttonPrimary, buttonSecondary

The button parts apply to every c15t button of that style, in the banner and the dialog. The script tag's banner has no rights group, so consentBannerRights does not apply there.

Preference dialog parts

Partcomponents keytheme.slots keydata-testid
Positionerdialog.rootNone
Dialog containerdialog.containerconsentDialogconsent-dialog-root
Carddialog.cardconsentDialogCard
Headerdialog.headerconsentDialogHeader
Titledialog.titleconsentDialogTitle
Descriptiondescription.dialogconsentDialogDescription
Contentdialog.contentconsentDialogContent
Backdropdialog.overlayconsentDialogOverlay
"Secured by" tagtag.dialogconsentDialogTag

The stock dialog's footer is the preference widget's footer. Style it with consentWidgetFooter. There is no consentDialogFooter slot.

Preference widget parts

The widget is the list of categories inside the dialog. React and Vue call it manager.

Partcomponents keytheme.slots key
Widget rootmanager.rootconsentWidget
Footermanager.footerconsentWidgetFooter
Group of action groupsmanager.actionsNone
One action groupmanager.actionGroupconsentWidgetFooterSubGroup
Category accordionaccordion.root, accordion.triggerRow, accordion.arrow, accordion.header, accordion.title, accordion.control, accordion.contentViewport, accordion.contentInnerconsentWidgetAccordion
One accordion itemaccordion-item.root, accordion-item.trigger, accordion-item.contentNone
Category switchswitch.root, switch.track, switch.thumbtoggle
Vendor listvendor-list.root, vendor-list.trigger, vendor-list.content, vendor-list.item, vendor-list.header, vendor-list.name, vendor-list.description, vendor-list.link, vendor-list.controlNone
"Secured by" tagtag.manager in React, tag.dialog in VueconsentWidgetTag

Floating trigger parts

Partcomponents keytheme.slots key
Buttontrigger.rootconsentDialogTrigger
Icontrigger.iconconsentDialogTriggerIcon
Labeltrigger.textNone
Toolbar, its items and iconstrigger.toolbar, trigger.toolbarItem, trigger.toolbarIconconsentDialogTriggerToolbar, consentDialogTriggerToolbarItem, consentDialogTriggerToolbarIcon

React renders the toolbar from ConsentDialogTriggerToolbar, and while ConsentDevTools is mounted. Vue renders it only while ConsentDevTools is mounted. In Astro, the toolbar slots reach the React dialog.

Componentcomponents keystheme.slots keys
Legal linkslegal-links.root, and link.banner, link.dialog, link.manager for one linkNone
IAB banneriab-banner.root, cardShell, card, header, title, description, partnersLink, purposeList, purposeMore, legitimateInterestNotice, footer, actions, actionGroup, overlayiabConsentBanner, iabConsentBannerCard, iabConsentBannerHeader, iabConsentBannerFooter, iabConsentBannerTag, iabConsentBannerOverlay
IAB dialogiab-dialog.root, card, header, headerContent, title, description, closeButton, body, content, loading, footer, tabs, tabsList, tabTrigger, tabIndicator, tabPanel, specialPurposes, consentNotice, actions, actionGroup, overlayiabConsentDialog, iabConsentDialogCard, iabConsentDialogHeader, iabConsentDialogFooter, iabConsentDialogTag, iabConsentDialogOverlay
IAB purpose, stack and vendor rowsiab-purpose-item.*, iab-stack-item.*, iab-vendor-list.*None

Legal links take no theme.slots key. They keep their stock link class, and a description slot's classes stay on the description around them.

ConsentGate placeholder parts

ConsentGate shows a placeholder card while its category is denied. React, Next.js, TanStack Start, Vue, Nuxt, Svelte and SvelteKit render the same three parts:

Partcomponents keytheme.slots keydata-testid
Placeholder cardconsent-gate.rootconsentGateconsent-gate-placeholder
Titleconsent-gate.titleconsentGateTitleconsent-gate-title
Button that opens preferencesconsent-gate.buttonconsentGateButtonconsent-gate-button

The button part applies on top of button.primary, or buttonPrimary. The parts only reach the built-in placeholder. A placeholder you pass yourself takes your own classes, and the wrapper around the gate takes className in React or class in Svelte. Astro, HTML and JavaScript have no ConsentGate.

Style parts from outside the script tag's shadow root

The script tag and init() render into a shadow root. Every part that has a theme.slots key carries it in a part attribute, so page CSS can reach it with ::part():

[data-c15t-ui]::part(consentBannerCard) {
  border: 3px solid #6943a3;
}

Select on state attributes

Parts carry data-* attributes that describe their state. Select on these instead of guessing at markup:

ElementAttributes
Banner rootdata-prompt, data-model, data-variant, data-position, data-blocking
Action buttonsdata-action (accept, reject, customize, dismiss or save), data-variant, data-mode
Footer and action groupsdata-direction, data-fill, data-split
Right linksdata-action="right", data-right
Preference dialogdata-state (open or closed), on the elements listed in motion
Category switchdata-state, data-disabled

The script tag's banner root has data-variant, data-position and data-blocking only, with data-blocking set to "true" or "false". Its right links carry data-action="customize".

The banner card does not inherit the root's attributes. With Tailwind, give the root group and read the root's attribute from a child part:

components: {
  banner: {
    root: { className: 'group' },
    card: { className: 'group-data-[variant=bar]:rounded-none' },
  },
},

Test notices as well as choice prompts. A notice has an acknowledge action, data-action="dismiss", instead of accept and reject.

Do not target c15t's class names

c15t's stock class names are generated from CSS Modules and end in a hash, such as c15t-ui-card-lgjVq. The hash changes whenever the source CSS changes, and the classes disappear under noStyle. A rule that targets one breaks on an upgrade without warning. Target a part API, data-testid, a data-* attribute or ::part() instead.

Remove c15t's classes with noStyle

noStyle renders the same markup and behavior without c15t's stock classes. It keeps your part classes and styles, data-testid and the data-* attributes. It does not replace layout, spacing, focus indicators or responsive behavior, so use it when you intend to write all of that yourself.

FrameworkWhere
Next.js, TanStack Start, ReactnoStyle in the provider options for every component, or the noStyle prop on ConsentBanner, ConsentDialog, ConsentWidget, ConsentDialogTrigger and the IAB components
Svelte, SvelteKitnoStyle in the provider options, or the prop on a stock component
AstronoStyle prop on ConsentBanner and IABConsentBanner
HTML, JavaScriptui.noStyle, which also drops the bundled stylesheet
Vue, NuxtThe noStyle prop on ConsentWidget, and on the PreferenceItemRoot and PreferenceItemContent primitives, whose other parts follow the root. The banner, dialog and trigger have no noStyle

In theme.slots, { noStyle: true } on one slot replaces that part's stock classes in Svelte, Astro's banner and the script tag. React and Vue, including Astro's React and Vue dialog islands, ignore a slot's noStyle, so its classes apply on top of the stock ones. If the markup itself must change, use your framework's compound components or headless API.