Skip to main content

Customization

Theme tokens

Where tokens come from

c15t's components take their colors from --c15t-* CSS variables, and most of their fonts, radii, spacing, shadows and motion too. c15t's rules set the defaults. You change them with a theme object or with CSS, and every part that reads a token changes with it. Some values are still fixed for one component, such as button padding, the radius of the "Secured by" tag and several font sizes and weights, so a token change does not move them. Restyle those parts through slots or your own CSS. Stylesheets and CSS layers covers how c15t loads its rules, and dark mode covers the dark set of tokens.

Set semantic values together

This React theme changes the brand color and large corner radius. Include hover and foreground colors when overriding raw variables so the button remains readable in each state.

import { defineTheme } from '@c15t/ui/theme';

export const theme = defineTheme({
	colors: { primary: '#2f6f4e' },
	radius: { lg: '4px' },
	consentActions: {
		primary: { variant: 'primary', mode: 'filled' },
		dismiss: { variant: 'neutral', mode: 'stroke' },
	},
});

Install @c15t/ui if importing its theme helper directly. Where the theme goes depends on the framework:

FrameworkTokensconsentActions
Next.js, TanStack Start, React<ConsentTheme theme={theme} />, rendered on the server where the app has onetheme in the provider options
Nuxt, Vuetheme or tokens in the module or plugin optionsNot available
Astrotheme in the integration optionsThe same theme
Svelte, SvelteKitgenerateThemeCSS(theme) from @c15t/ui/theme on the server, in a <style> element, or --c15t-* variables in your stylesheettheme on ConsentProvider (Svelte) or ConsentRoot (SvelteKit)
HTML, JavaScriptui.themeNot available

React and Svelte providers do not turn tokens in their theme option into CSS, and warn in development when a theme holds tokens but the page has no <style id="c15t-theme">. Your framework's customize page shows the full setup.

consentActions selects styling by action role. A per-action entry overrides primary, which overrides default.

Combine a generated theme with your own CSS

generateThemeCSS writes its variables on :root:root and .c15t-theme-root.c15t-theme-root, more specific than c15t's defaults. The theme therefore overrides the defaults whether its <style> element comes before or after c15t's rules. The same output backs ConsentTheme in React, Next.js and TanStack Start, Astro's theme option and the script tag's ui.theme.

A --c15t-* variable you set on plain :root in your own CSS loses to a generated theme that sets the same variable, even when your rule loads later. Put the value in the theme, or raise your selector:

:root:root {
  --c15t-primary: #2f6f4e;
}

Scoped rules such as [data-prompt] { --c15t-primary: ... } set the variable on the banner element itself, so they still apply inside it.

Every token

Every framework uses the same tokens. A theme object, such as ConsentTheme's theme, Astro's and Vue's theme or the script tag's ui.theme, takes the theme key, such as radius.lg. Vue and Nuxt tokens take the CSS variable name without the leading --, such as c15t-radius-lg. A stylesheet sets the CSS variable itself. Your framework's customize page shows where each one goes. Motion and animation explains the duration and easing tokens.

CSS variableTheme keyDefault
--c15t-primarycolors.primaryhsl(228, 100%, 60%)
--c15t-primary-hovercolors.primaryHoverhsl(228, 100%, 55%)
--c15t-surfacecolors.surfacehsl(0, 0%, 100%)
--c15t-surface-hovercolors.surfaceHoverhsl(0, 0%, 98%)
--c15t-bordercolors.borderhsl(0, 0%, 90%)
--c15t-border-hovercolors.borderHoverhsl(0, 0%, 85%)
--c15t-textcolors.texthsl(0, 0%, 10%)
--c15t-text-mutedcolors.textMutedhsl(0, 0%, 40%)
--c15t-text-on-primarycolors.textOnPrimaryauto-derived from colors.primary when omitted
--c15t-overlaycolors.overlayhsla(0, 0%, 0%, 0.5)
--c15t-switch-trackcolors.switchTrackhsl(0, 0%, 85%)
--c15t-switch-track-activecolors.switchTrackActivecolors.primary
--c15t-switch-thumbcolors.switchThumbhsl(0, 0%, 100%)
--c15t-font-familytypography.fontFamilysystem-ui, -apple-system, sans-serif
--c15t-font-size-smtypography.fontSize.sm0.875rem
--c15t-font-size-basetypography.fontSize.base1rem
--c15t-font-size-lgtypography.fontSize.lg1.125rem
--c15t-font-weight-normaltypography.fontWeight.normal400
--c15t-font-weight-mediumtypography.fontWeight.medium500
--c15t-font-weight-semiboldtypography.fontWeight.semibold600
--c15t-line-height-tighttypography.lineHeight.tight1.25
--c15t-line-height-normaltypography.lineHeight.normal1.5
--c15t-line-height-relaxedtypography.lineHeight.relaxed1.75
--c15t-space-xsspacing.xs0.25rem
--c15t-space-smspacing.sm0.5rem
--c15t-space-mdspacing.md1rem
--c15t-space-lgspacing.lg1.5rem
--c15t-space-xlspacing.xl2rem
--c15t-radius-smradius.sm0.25rem
--c15t-radius-mdradius.md0.5rem
--c15t-radius-lgradius.lg0.75rem
--c15t-radius-fullradius.full9999px
--c15t-shadow-smshadows.sm0 1px 2px hsla(0, 0%, 0%, 0.05)
--c15t-shadow-mdshadows.md0 4px 12px hsla(0, 0%, 0%, 0.08)
--c15t-shadow-lgshadows.lg0 8px 24px hsla(0, 0%, 0%, 0.12)
--c15t-duration-fastmotion.duration.fast80ms
--c15t-duration-normalmotion.duration.normal150ms
--c15t-duration-slowmotion.duration.slow200ms
--c15t-easingmotion.easingcubic-bezier(0.4, 0, 0.2, 1)
--c15t-easing-outmotion.easingOutcubic-bezier(0.215, 0.61, 0.355, 1)
--c15t-easing-inmotion.easingIncubic-bezier(0.55, 0.055, 0.675, 0.19)
--c15t-easing-in-outmotion.easingInOutcubic-bezier(0.645, 0.045, 0.355, 1)
--c15t-easing-springmotion.easingSpringcubic-bezier(0.34, 1.56, 0.64, 1)

The radius tokens round different parts. radius.lg rounds the banner card, the preference dialog, ConsentGate placeholders and the floating trigger. radius.md rounds buttons, accordions, tabs and the vendor list. radius.sm rounds small parts inside the banner and dialog. To give the banner and its buttons the same 4px corners, set both lg and md.

Target a prompt with CSS

[data-prompt][data-model='opt-in'] {
  --c15t-primary: #2f6f4e;
  --c15t-primary-hover: #24563c;
  --c15t-text-on-primary: #fff;
}

Use attributes exposed by the rendered component, not guessed class names. The script tag's banner has no data-prompt or data-model. Test the prompt and the preferences dialog separately because tokens scoped to one prompt do not automatically reach a portaled dialog.

Size variableDefaultTarget
--consent-banner-max-width440pxFloating card
--consent-banner-widget-max-width20remWidget
--consent-banner-wall-max-width30remChoice wall

The banner footer lays out its actions by the card's width, not the viewport's. With the default compact profile, a card narrower than 22rem puts Reject and Accept on one row and Customize on a full-width row below them, on any screen size.

Test long translations and small screens after changing width or typography. A compact banner must still fit the required actions.

Restyle the "Secured by" tag

The tag sits on the edge of the banner and dialog cards and uses the primary color by default. Set these variables on :root, or on an element that contains the tag. The dialog renders in a portal, so a variable set on the banner does not reach the dialog's tag.

VariableDefaultTarget
--consent-branding-tag-background-colorvar(--c15t-primary)Tag background
--consent-branding-tag-border-color--c15t-primary mixed 14% toward blackTag border
--consent-branding-tag-text-colorvar(--c15t-text-on-primary, #fff)"Secured by" and the wordmark
--consent-branding-tag-mark-colorThe text colorc15t mark or inth logo
--consent-branding-tag-shadowInset highlight and a 1px drop shadowTag shadow
--consent-branding-tag-attached-edge-width0pxBorder on the edge that meets the card

The stylesheet does not declare these variables. Each default resolves on the tag, so a --c15t-primary you scope to a banner still colors the tag.

This makes the tag look like a tab of the card:

:root {
  --consent-branding-tag-background-color: var(--c15t-surface);
  --consent-branding-tag-border-color: var(--c15t-border);
  --consent-branding-tag-text-color: var(--c15t-text-muted);
  --consent-branding-tag-mark-color: var(--c15t-primary);
  --consent-branding-tag-shadow: none;
}

The edge that meets the card has no border by default. Above the banner the tag overlaps the card's top border by 1px and covers it. Below the dialog the tag starts under the card's bottom border. Set --consent-branding-tag-attached-edge-width: 1px to draw that edge. It is drawn over the card's border, so the two borders do not stack.