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 variable | Theme key | Default | Used by |
|---|---|---|---|
--c15t-duration-fast | motion.duration.fast | 80ms | Banner fade out, buttons, tabs |
--c15t-duration-normal | motion.duration.normal | 150ms | Preference dialog, late banner fade in, switches, accordions |
--c15t-duration-slow | motion.duration.slow | 200ms | Legal links, floating trigger |
--c15t-easing | motion.easing | cubic-bezier(0.4, 0, 0.2, 1) | Switches, accordions |
--c15t-easing-out | motion.easingOut | cubic-bezier(0.215, 0.61, 0.355, 1) | Preference dialog, late banner fade in, floating trigger hover |
--c15t-easing-in | motion.easingIn | cubic-bezier(0.55, 0.055, 0.675, 0.19) | Banner fade out |
--c15t-easing-in-out | motion.easingInOut | cubic-bezier(0.645, 0.045, 0.355, 1) | Floating trigger snapping to a corner |
--c15t-easing-spring | motion.easingSpring | cubic-bezier(0.34, 1.56, 0.64, 1) | The dialog's scale in |
This theme slows the dialog down and removes its overshoot:
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:
| Framework | For every surface | For one surface |
|---|---|---|
| Next.js, TanStack Start | options.disableAnimation in Next.js c15t.config.ts, or on ConsentRoot | disableAnimation prop on ConsentBanner, ConsentDialog, IABConsentBanner, IABConsentDialog |
| React | disableAnimation in the ConsentProvider options | The same component props |
| Nuxt | disableAnimation in the c15t module options | disableAnimation prop on consent-banner.vue, consent-manager.vue and the IAB banner and dialog, when you render them yourself |
| Vue | disableAnimation in the c15tVue options | The same component props |
| Astro | disableAnimation in the c15t() integration options | disableAnimation prop on ConsentBanner, ConsentDialog, IABConsentBanner, IABConsentDialog |
| Svelte, SvelteKit | disableAnimation on ConsentProvider (Svelte) or ConsentRoot (SvelteKit) | disableAnimation prop on ConsentBanner, ConsentDialog, IABConsentBanner, IABConsentDialog |
| HTML | data-disable-animation on the script tag, or ui.disableAnimation in config | ui.banner.disableAnimation, ui.dialog.disableAnimation |
| JavaScript | ui.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:
| Framework | Elements with data-state |
|---|---|
| HTML, JavaScript | Dialog overlay, positioner and content |
| Svelte, SvelteKit | Dialog backdrop, positioner and content |
| Vue, Nuxt | Dialog content and overlay. The dialog unmounts when it closes, so you only see open |
| React, Next.js, TanStack Start | Not 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:
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
- In DevTools, open the Rendering panel and emulate
prefers-reduced-motion: reduce. Reload with site data cleared. - The banner appears without sliding in.
- Open the preference dialog and toggle a switch. The switch changes without animating.
- Turn the emulation off, set a slower
motion.duration.normal, and open the dialog again. It fades in at the new speed.