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.
Install @c15t/ui if importing its theme helper directly. Where the theme
goes depends on the framework:
| Framework | Tokens | consentActions |
|---|---|---|
| Next.js, TanStack Start, React | <ConsentTheme theme={theme} />, rendered on the server where the app has one | theme in the provider options |
| Nuxt, Vue | theme or tokens in the module or plugin options | Not available |
| Astro | theme in the integration options | The same theme |
| Svelte, SvelteKit | generateThemeCSS(theme) from @c15t/ui/theme on the server, in a <style> element, or --c15t-* variables in your stylesheet | theme on ConsentProvider (Svelte) or ConsentRoot (SvelteKit) |
| HTML, JavaScript | ui.theme | Not 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:
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 variable | Theme key | Default |
|---|---|---|
--c15t-primary | colors.primary | hsl(228, 100%, 60%) |
--c15t-primary-hover | colors.primaryHover | hsl(228, 100%, 55%) |
--c15t-surface | colors.surface | hsl(0, 0%, 100%) |
--c15t-surface-hover | colors.surfaceHover | hsl(0, 0%, 98%) |
--c15t-border | colors.border | hsl(0, 0%, 90%) |
--c15t-border-hover | colors.borderHover | hsl(0, 0%, 85%) |
--c15t-text | colors.text | hsl(0, 0%, 10%) |
--c15t-text-muted | colors.textMuted | hsl(0, 0%, 40%) |
--c15t-text-on-primary | colors.textOnPrimary | auto-derived from colors.primary when omitted |
--c15t-overlay | colors.overlay | hsla(0, 0%, 0%, 0.5) |
--c15t-switch-track | colors.switchTrack | hsl(0, 0%, 85%) |
--c15t-switch-track-active | colors.switchTrackActive | colors.primary |
--c15t-switch-thumb | colors.switchThumb | hsl(0, 0%, 100%) |
--c15t-font-family | typography.fontFamily | system-ui, -apple-system, sans-serif |
--c15t-font-size-sm | typography.fontSize.sm | 0.875rem |
--c15t-font-size-base | typography.fontSize.base | 1rem |
--c15t-font-size-lg | typography.fontSize.lg | 1.125rem |
--c15t-font-weight-normal | typography.fontWeight.normal | 400 |
--c15t-font-weight-medium | typography.fontWeight.medium | 500 |
--c15t-font-weight-semibold | typography.fontWeight.semibold | 600 |
--c15t-line-height-tight | typography.lineHeight.tight | 1.25 |
--c15t-line-height-normal | typography.lineHeight.normal | 1.5 |
--c15t-line-height-relaxed | typography.lineHeight.relaxed | 1.75 |
--c15t-space-xs | spacing.xs | 0.25rem |
--c15t-space-sm | spacing.sm | 0.5rem |
--c15t-space-md | spacing.md | 1rem |
--c15t-space-lg | spacing.lg | 1.5rem |
--c15t-space-xl | spacing.xl | 2rem |
--c15t-radius-sm | radius.sm | 0.25rem |
--c15t-radius-md | radius.md | 0.5rem |
--c15t-radius-lg | radius.lg | 0.75rem |
--c15t-radius-full | radius.full | 9999px |
--c15t-shadow-sm | shadows.sm | 0 1px 2px hsla(0, 0%, 0%, 0.05) |
--c15t-shadow-md | shadows.md | 0 4px 12px hsla(0, 0%, 0%, 0.08) |
--c15t-shadow-lg | shadows.lg | 0 8px 24px hsla(0, 0%, 0%, 0.12) |
--c15t-duration-fast | motion.duration.fast | 80ms |
--c15t-duration-normal | motion.duration.normal | 150ms |
--c15t-duration-slow | motion.duration.slow | 200ms |
--c15t-easing | motion.easing | cubic-bezier(0.4, 0, 0.2, 1) |
--c15t-easing-out | motion.easingOut | cubic-bezier(0.215, 0.61, 0.355, 1) |
--c15t-easing-in | motion.easingIn | cubic-bezier(0.55, 0.055, 0.675, 0.19) |
--c15t-easing-in-out | motion.easingInOut | cubic-bezier(0.645, 0.045, 0.355, 1) |
--c15t-easing-spring | motion.easingSpring | cubic-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
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 variable | Default | Target |
|---|---|---|
--consent-banner-max-width | 440px | Floating card |
--consent-banner-widget-max-width | 20rem | Widget |
--consent-banner-wall-max-width | 30rem | Choice 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.
| Variable | Default | Target |
|---|---|---|
--consent-branding-tag-background-color | var(--c15t-primary) | Tag background |
--consent-branding-tag-border-color | --c15t-primary mixed 14% toward black | Tag border |
--consent-branding-tag-text-color | var(--c15t-text-on-primary, #fff) | "Secured by" and the wordmark |
--consent-branding-tag-mark-color | The text color | c15t mark or inth logo |
--consent-branding-tag-shadow | Inset highlight and a 1px drop shadow | Tag shadow |
--consent-branding-tag-attached-edge-width | 0px | Border 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:
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.