---
title: Component parts
description: Find every part of c15t's banner, dialog, widget, trigger and
  ConsentGate placeholder, its key in your framework's part API, and the data
  attributes to select on.
group: customization
lastModified: "2026-10-10T16:01:45+01:00"
---
## 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.

|Framework|Part API|Keys look like|Class key|
|--|--|--|--|
|Next.js, TanStack Start|`options.components` in Next.js `c15t.config.ts`, or on `ConsentRoot`|`components.banner.card`|`className`|
|React|`components` in the `ConsentProvider` options|`components.banner.card`|`className`|
|Nuxt|`components` in the `c15t` options, or in `app/app.config.ts`|`components.banner.card`|`class`|
|Vue|`components` in the `c15tVue` options|`components.banner.card`|`class`|
|Astro|`theme.slots` in the `c15t()` integration options|`slots.consentBannerCard`|A string, or `className`|
|Svelte, SvelteKit|`theme.slots` on `ConsentProvider` (Svelte) or `ConsentRoot` (SvelteKit)|`slots.consentBannerCard`|A string, or `className`|
|HTML, JavaScript|`ui.theme.slots`|`slots.consentBannerCard`|A 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:

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

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

```ts
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](/docs/customization/class-names) covers CSS
Modules, vanilla-extract, StyleX and Emotion, and
[Tailwind CSS](/docs/customization/tailwind) covers utilities on parts.

## Banner parts

|Part|`components` key|`theme.slots` key|`data-testid`|
|--|--|--|--|
|Positioning root, with the state attributes|`banner.root`|`consentBanner`|`consent-banner-root`|
|Card sizing and branding container|`banner.cardShell`|None||
|Visible card|`banner.card`|`consentBannerCard`|`consent-banner-card`|
|Title and description container|`banner.header`|`consentBannerHeader`||
|Title|`banner.title`|`consentBannerTitle`||
|Description|`description.banner`|`consentBannerDescription`||
|Action area|`banner.footer`|`consentBannerFooter`||
|Group of action groups|`banner.actions`|None||
|One group of equally prominent actions|`banner.actionGroup`|`consentBannerFooterSubGroup`||
|Group of right links|`banner.rights`|`consentBannerRights`||
|One right link|`banner.rightLink`|`consentBannerRightLink`||
|Backdrop of a blocking banner|`banner.overlay`|`consentBannerOverlay`||
|"Secured by" tag|`tag.banner`|`consentBannerTag`||
|Filled and outlined buttons|`button.primary`, `button.secondary`|`buttonPrimary`, `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

|Part|`components` key|`theme.slots` key|`data-testid`|
|--|--|--|--|
|Positioner|`dialog.root`|None||
|Dialog container|`dialog.container`|`consentDialog`|`consent-dialog-root`|
|Card|`dialog.card`|`consentDialogCard`||
|Header|`dialog.header`|`consentDialogHeader`||
|Title|`dialog.title`|`consentDialogTitle`||
|Description|`description.dialog`|`consentDialogDescription`||
|Content|`dialog.content`|`consentDialogContent`||
|Backdrop|`dialog.overlay`|`consentDialogOverlay`||
|"Secured by" tag|`tag.dialog`|`consentDialogTag`||

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

|Part|`components` key|`theme.slots` key|
|--|--|--|
|Widget root|`manager.root`|`consentWidget`|
|Footer|`manager.footer`|`consentWidgetFooter`|
|Group of action groups|`manager.actions`|None|
|One action group|`manager.actionGroup`|`consentWidgetFooterSubGroup`|
|Category accordion|`accordion.root`, `accordion.triggerRow`, `accordion.arrow`, `accordion.header`, `accordion.title`, `accordion.control`, `accordion.contentViewport`, `accordion.contentInner`|`consentWidgetAccordion`|
|One accordion item|`accordion-item.root`, `accordion-item.trigger`, `accordion-item.content`|None|
|Category switch|`switch.root`, `switch.track`, `switch.thumb`|`toggle`|
|Vendor list|`vendor-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.control`|None|
|"Secured by" tag|`tag.manager` in React, `tag.dialog` in Vue|`consentWidgetTag`|

## Floating trigger parts

|Part|`components` key|`theme.slots` key|
|--|--|--|
|Button|`trigger.root`|`consentDialogTrigger`|
|Icon|`trigger.icon`|`consentDialogTriggerIcon`|
|Label|`trigger.text`|None|
|Toolbar, its items and icons|`trigger.toolbar`, `trigger.toolbarItem`, `trigger.toolbarIcon`|`consentDialogTriggerToolbar`, `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.

## Legal links and IAB TCF parts

|Component|`components` keys|`theme.slots` keys|
|--|--|--|
|Legal links|`legal-links.root`, and `link.banner`, `link.dialog`, `link.manager` for one link|None|
|IAB banner|`iab-banner.root`, `cardShell`, `card`, `header`, `title`, `description`, `partnersLink`, `purposeList`, `purposeMore`, `legitimateInterestNotice`, `footer`, `actions`, `actionGroup`, `overlay`|`iabConsentBanner`, `iabConsentBannerCard`, `iabConsentBannerHeader`, `iabConsentBannerFooter`, `iabConsentBannerTag`, `iabConsentBannerOverlay`|
|IAB dialog|`iab-dialog.root`, `card`, `header`, `headerContent`, `title`, `description`, `closeButton`, `body`, `content`, `loading`, `footer`, `tabs`, `tabsList`, `tabTrigger`, `tabIndicator`, `tabPanel`, `specialPurposes`, `consentNotice`, `actions`, `actionGroup`, `overlay`|`iabConsentDialog`, `iabConsentDialogCard`, `iabConsentDialogHeader`, `iabConsentDialogFooter`, `iabConsentDialogTag`, `iabConsentDialogOverlay`|
|IAB purpose, stack and vendor rows|`iab-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:

|Part|`components` key|`theme.slots` key|`data-testid`|
|--|--|--|--|
|Placeholder card|`consent-gate.root`|`consentGate`|`consent-gate-placeholder`|
|Title|`consent-gate.title`|`consentGateTitle`|`consent-gate-title`|
|Button that opens preferences|`consent-gate.button`|`consentGateButton`|`consent-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()`:

```css
[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:

|Element|Attributes|
|--|--|
|Banner root|`data-prompt`, `data-model`, `data-variant`, `data-position`, `data-blocking`|
|Action buttons|`data-action` (`accept`, `reject`, `customize`, `dismiss` or `save`), `data-variant`, `data-mode`|
|Footer and action groups|`data-direction`, `data-fill`, `data-split`|
|Right links|`data-action="right"`, `data-right`|
|Preference dialog|`data-state` (`open` or `closed`), on the elements listed in [motion](/docs/customization/motion#animate-your-own-rules-on-dialog-state)|
|Category switch|`data-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:

```tsx
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.

|Framework|Where|
|--|--|
|Next.js, TanStack Start, React|`noStyle` in the provider options for every component, or the `noStyle` prop on `ConsentBanner`, `ConsentDialog`, `ConsentWidget`, `ConsentDialogTrigger` and the IAB components|
|Svelte, SvelteKit|`noStyle` in the provider options, or the prop on a stock component|
|Astro|`noStyle` prop on `ConsentBanner` and `IABConsentBanner`|
|HTML, JavaScript|`ui.noStyle`, which also drops the bundled stylesheet|
|Vue, Nuxt|The `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.
