Skip to main content

Customization

Motion and animation

Change the speed with motion tokens

c15t animates its surfaces with three durations and five easing curves. Set them in the theme's motion object, or as CSS variables:

CSS variableTheme keyDefaultUsed by
--c15t-duration-fastmotion.duration.fast80msBanner fade out, buttons, tabs
--c15t-duration-normalmotion.duration.normal150msPreference dialog, late banner fade in, switches, accordions
--c15t-duration-slowmotion.duration.slow200msLegal links, floating trigger
--c15t-easingmotion.easingcubic-bezier(0.4, 0, 0.2, 1)Switches, accordions
--c15t-easing-outmotion.easingOutcubic-bezier(0.215, 0.61, 0.355, 1)Preference dialog, late banner fade in, floating trigger hover
--c15t-easing-inmotion.easingIncubic-bezier(0.55, 0.055, 0.675, 0.19)Banner fade out
--c15t-easing-in-outmotion.easingInOutcubic-bezier(0.645, 0.045, 0.355, 1)Floating trigger snapping to a corner
--c15t-easing-springmotion.easingSpringcubic-bezier(0.34, 1.56, 0.64, 1)The dialog's scale in

This theme slows the dialog down and removes its overshoot:

consent-theme.ts
export const theme = {
	motion: {
		duration: { fast: '120ms', normal: '220ms' },
		easingSpring: 'cubic-bezier(0.215, 0.61, 0.355, 1)',
	},
};

The banner never slides in. When it shows with the page, it is part of the first paint and appears on its first frame. When it arrives more than 100ms after the page first painted, such as after a slow script or a client-side init, it fades in over --c15t-duration-normal instead, and its root and backdrop carry data-entry="late". A banner rendered on the server always shows at once. When it hides, Vue, Svelte and the script tag fade it out over --c15t-duration-fast.

The theme goes where your framework's other tokens go. See theme tokens and your framework's customize page. In Vue and Nuxt, set tokens with the variable name without the leading --, such as 'c15t-duration-normal': '220ms'.

Turn animations off

disableAnimation removes the banner's fades, the enter and exit transitions of the dialogs, and the hover and snap transitions of the floating ConsentDialogTrigger. Set it once for every surface, then override it for one surface where your framework allows:

FrameworkFor every surfaceFor one surface
Next.js, TanStack Startoptions.disableAnimation in Next.js c15t.config.ts, or on ConsentRootdisableAnimation prop on ConsentBanner, ConsentDialog, IABConsentBanner, IABConsentDialog
ReactdisableAnimation in the ConsentProvider optionsThe same component props
NuxtdisableAnimation in the c15t module optionsdisableAnimation prop on consent-banner.vue, consent-manager.vue and the IAB banner and dialog, when you render them yourself
VuedisableAnimation in the c15tVue optionsThe same component props
AstrodisableAnimation in the c15t() integration optionsdisableAnimation prop on ConsentBanner, ConsentDialog, IABConsentBanner, IABConsentDialog
Svelte, SvelteKitdisableAnimation on ConsentProvider (Svelte) or ConsentRoot (SvelteKit)disableAnimation prop on ConsentBanner, ConsentDialog, IABConsentBanner, IABConsentDialog
HTMLdata-disable-animation on the script tag, or ui.disableAnimation in configui.banner.disableAnimation, ui.dialog.disableAnimation
JavaScriptui.disableAnimation in init()ui.banner.disableAnimation, ui.dialog.disableAnimation

A value on one surface wins over the value for every surface.

Vue's ConsentRoot renders the banner and dialog without props, so in Vue and Nuxt a per-surface value only applies when you render the surface components yourself.

Follow the visitor's reduced motion setting

c15t's stylesheet stops the banner, dialog, floating trigger, switches, tabs and accordions from animating while the visitor asks for reduced motion. The rules sit in a prefers-reduced-motion: reduce media query, so they apply in every framework and follow the setting as it changes, with no option to set.

disableAnimation: false does not bring the animations back for these visitors, because the stylesheet rule applies whatever the option says.

Animate your own rules on dialog state

The preference dialog marks its open state with data-state, open or closed, so a rule can animate your own additions to it. Which element carries the attribute depends on the framework:

FrameworkElements with data-state
HTML, JavaScriptDialog overlay, positioner and content
Svelte, SvelteKitDialog backdrop, positioner and content
Vue, NuxtDialog content and overlay. The dialog unmounts when it closes, so you only see open
React, Next.js, TanStack StartNot on ConsentDialog. The Dialog primitive's trigger, overlay, content and close parts carry it

Banners do not carry data-state. Read data-prompt, data-variant and the other attributes in component parts instead.

Stop transitions while you switch themes

Every c15t stylesheet has a c15t-no-transitions class that sets transition and animation to none on an element and its children. Add it to <html> while your app swaps themes, so colors change in one frame. Force a style and layout pass before you remove it. Otherwise the browser computes the new theme only after the class is gone, and the change animates:

const root = document.documentElement;
root.classList.add('c15t-no-transitions');
applyYourTheme();
// Reading layout applies the new theme while transitions are off.
root.getBoundingClientRect();
root.classList.remove('c15t-no-transitions');

This assumes applyYourTheme changes classes or custom properties synchronously. If your framework applies the theme in a later render, run the last two lines after that render commits.

Check the result

  1. In DevTools, open the Rendering panel and emulate prefers-reduced-motion: reduce. Reload with site data cleared.
  2. The banner appears without sliding in.
  3. Open the preference dialog and toggle a switch. The switch changes without animating.
  4. Turn the emulation off, set a slower motion.duration.normal, and open the dialog again. It fades in at the new speed.