Skip to content

Motion ​

CDS 4 has a single motion scale: seven durations, a set of easing curves and two loops. It is the same for every theme (cds, b2b, promo, luma). You can use it four ways: ready-made Vue transitions, a Sass module, CSS variables and the motion JS object.

Take steps from the scale instead of literals like 0.2s ease-in-out. Your animations then run at the same pace as CDS components, and changing a value in the scale reaches every place at once.

Duration scale ​

Press «Запустить» (Run): the markers travel the same distance, each in the time of its step.

NameValueUsed for
instant50mspress
very-fast100msentering hover, popover and checkmark disappearing
fast200msleaving hover, color and selection changes, appearance
normal300msresizing a large block, modal and drawer appearance
slow400msexpanding a large element
very-slow500msbackdrop dimming
spring300msthe time a spring takes to settle

Loops sit outside the scale: the scale answers a user action, while a loop runs on its own. A pulse lasts 2s, a ripple 1.5s.

Easing curves ​

The same duration on different curves: enter rushes in and brakes, exit accelerates and leaves, standard starts softly and stops softly. Switch the duration to compare the curves over a short and a long run.

NameValueUsed for
standardcubic-bezier(0.2, 0, 0, 1)default: in-place changes and movement
entercubic-bezier(0.05, 0.7, 0.1, 1)appearance
exitcubic-bezier(0.3, 0, 1, 1)leaving only
springlinear(…), M3 fast-spatial springa small object appearing with a bounce
loopcubic-bezier(0.37, 0, 0.63, 1)the pulse loop
ripplecubic-bezier(0, 0, 0.2, 1)the ripple loop
linearlineartimer: the tick step equals the duration

The spring bounce belongs to scale and position only. Use the spring on transform, not on opacity or background-color: color and opacity never bounce.

What to choose ​

CaseDurationCurve
pressinstantstandard
entering hoververy-faststandard
leaving hover, selection change, colorfaststandard
popover, dropdown, toast appearingfastenter
popover, checkmark leavingvery-fastexit
resizing a large blocknormalstandard
modal, drawer: appear / leavenormal/fastenter / exit
pulse / ripple2s / 1.5sloop / ripple

Ready-made transitions ​

Transitions are wrapper components over Vue <Transition> and <TransitionGroup>: put an element with v-if or v-show inside, no classes or styles needed. Pick a transition and hide or show the card:

TransitionWhat it animatesTime and curveWhen to use
CdsTransitionFadeopacityfast enter / very-fast exitin-place appear and leave: hints, captions
CdsTransitionSlideY8px vertical shift and opacityfast enter / very-fast exitsmall blocks, dropdown content
CdsTransitionSlideX15px horizontal shift and opacityfast enter / very-fast exithorizontal content changes
CdsTransitionSlideRightfull-width shift to the right; the leaving block leaves the flownormal standardpaging forward: calendar, carousel
CdsTransitionSlideLeftfull-width shift to the left; the leaving block leaves the flownormal standardpaging back
CdsTransitionJumpRight15px shift to the right and opacity; the leaving block leaves the flownormal standarda value that "moves" forward: counters, captions
CdsTransitionJumpLeft15px shift to the left and opacity; the leaving block leaves the flownormal standardthe same in the other direction
CdsTransitionScalescale from zero and opacity; leaving is a fade onlyfast enter / very-fast exitpop-up menus, tags
CdsTransitionExpandheight (JS): vertical expand and collapsefast standardexpandable panels, accordion
CdsTransitionExpandXwidth (JS): the same horizontallyfast standardside panels, collapsible columns
CdsTransitionPresencefade, then collapse of height, spacing and borderleave very-fast exit + fast standard; appear fast standard + fast entera block leaves the flow and neighbors must move smoothly; this is how a CdsTree branch expands
CdsTransitionModalBottomslides in from the bottom of the screennormal enter / fast exita panel or window coming from the bottom
CdsTransitionModalTopslides in from the top of the screennormal enter / fast exita panel or window coming from the top
CdsModalTransitionthe modal unfolds from a source element (target prop)normal enter / fast exit; content normal×2 / fast×2 on standarda modal that "flies out" of a button or card

Props depend on the kind of transition:

  • CSS transitions (Fade, Slide*, Jump*, Scale, Modal*): mode, group (a list via TransitionGroup), disabled (no animation), hideOnLeave (hides the leaving element with display: none), leaveAbsolute (takes the element out of the flow while it leaves) and origin (the point scale starts from).
  • JS transitions (Expand, ExpandX): mode, disabled, group, hideOnLeave.
  • CdsTransitionPresence: appear, group, disabled — see below.
  • CdsModalTransition: a single target prop — the element or [x, y] coordinates the modal unfolds from.

You can play with every prop in Storybook, in the «Песочница: переходы» (Transitions playground) story.

What the transitions share:

  • A leaving element taken out of the flow (SlideRight, SlideLeft, Jump* and any CSS transition with leaveAbsolute) does not take clicks: a click will not land on something that is already leaving.
  • Under prefers-reduced-motion, shifts and scale are replaced with a fade. Expand and ExpandX change size instantly under reduced motion. JS transitions (Expand, ExpandX, CdsModalTransition) read the mode at mount, and disabled follows it by default.

CdsTransitionPresence: a block leaving the flow ​

When a notification, alert or list row leaves the page, a fade alone is not enough: a gap stays behind and the neighbors jump into it. Collapsing the height at the same time as the fade shows squashed text. CdsTransitionPresence does it in two phases — add and remove a few invoices:

  1. Leave. A very-fast fade on the exit curve, then a fast collapse on standard. Vertical spacing and the border collapse together with the height, so no gap remains.
  2. Enter runs in reverse: a fast expand on standard, then a fast fade-in on enter.
  3. Under prefers-reduced-motion only the fade stays; the space closes and opens instantly.

This is how CdsTree branches expand and CdsNotification leaves. Set the spacing between list blocks with margin, not with gap on the container: margin collapses together with the block, while gap disappears with a jump at the end of the leave.

PropWhat it does
appearplay the transition on the initial render
groupuse TransitionGroup — for a list of elements
disableddisable the animation: elements appear and disappear at once

Other Transition and TransitionGroup attributes (for example, tag on a list) are passed through. @after-leave fires after both phases — a handy point to emit a close event:

vue
<CdsTransitionPresence @after-leave="emit('closed')">
  <div v-if="visible" class="alert">Saved</div>
</CdsTransitionPresence>

Hook listeners (@enter, @after-leave and others) receive only the element.

Pulse and ripple ​

A pulse signals "happening now": the dot breathes with opacity. A ripple signals "something new": a ring under the dot grows twice its size and fades. Both loops are turned on where needed and do not run under prefers-reduced-motion. A ready-made example of a signaling loop is CdsStatus with the indicator and pulse props: the status dot breathes while the process is going. The pulse is endless, so under WCAG 2.2.2 turn it on only while the signal is relevant.

In SCSS the same loops come from mixins, which declare their keyframes themselves:

scss
@use '@central-design-system/components/styles/motion';

.status-dot {
  @include motion.pulse;
}

.status-dot__ring {
  @include motion.ripple;
}

Reduced motion ​

Under prefers-reduced-motion: reduce, shifts, scale and springs turn off. Opacity and color stay: state feedback does not disappear, and a color change does not cause vestibular discomfort.

Loops fall into two classes:

  • Decorative or signaling — pulse, ripple and the CdsSkeletonLoader shimmer. Turns off: the status reads without animation, and endless blinking is exactly what such a user asks to get rid of.
  • Progress indicator — CdsLoader, indeterminate CdsProgressBar. Keeps running: a frozen spinner reads as a hang. WCAG 2.2.2 treats such motion as essential.

Write your own progress indicator without spatial. DS components follow the same policy: the skeleton shimmer is off under reduced motion, while CdsLoader and the indeterminate CdsProgressBar keep running.

To check the behavior without changing system settings, use Chrome DevTools: menu ⋮ → More tools → Rendering → Emulate CSS media feature prefers-reduced-motion → reduce. The demos on this page switch right away.

If you enable the legacy reset ($legacy-reset: true), it does not impose the reduced-motion policy: the CDS 3 markup compatibility reset does not mute animations.

Reference ​

The values in the Sass module, the CSS variables and motion match — a test checks it.

Sass module ​

The module is a separate entry and pulls in nothing else, so it fits any SCSS in your project. The functions return plain values, not var(): you can do math with them in Sass (add up delays, sum phases).

scss
@use '@central-design-system/components/styles/motion';
CallReturns
motion.duration(<name>)the step duration, for example 200ms
motion.easing(<name>)the curve, for example cubic-bezier(0.2, 0, 0, 1)
motion.transition(<properties>, <duration>, <curve>, <delay>)a transition list: every property with one step
motion.interactive-transition(<properties>, <state>)the interactive step for the rest, hover or active state

A name that is not in the scale is a compile error listing the valid names. The property list of transition is required: the module never writes transition: all.

State change. transition takes space-separated properties, a duration and a curve (standard by default). Several calls join with commas:

scss
.tooltip {
  transition: motion.transition(opacity transform, fast, enter);

  &.is-leaving {
    transition: motion.transition(opacity, very-fast, exit);
  }
}

.handle {
  transition:
    motion.transition(left, fast),
    motion.transition(right, fast, $delay: very-fast);
}

Hover and press. @include motion.interactive(<properties>) spreads the steps over the states: leaving hover 200ms, entering 100ms, press 50ms. The browser takes the transition of the state the element moves into, so the steps live on the states. If the hover is on an ancestor and the transition on a descendant, take the step by hand:

scss
.chip {
  @include motion.interactive(background-color border-color color);
}

.card:hover .arrow {
  transition: motion.interactive-transition(transform, hover);
}

Scale and position. Put everything that moves or scales into spatial: under reduced motion it turns off, while color and opacity stay. If the DS itself sets a shift that would otherwise remain, pair it with reduced — rules for reduced-motion users only:

scss
.badge {
  @include motion.spatial {
    animation: pop motion.duration(spring) motion.easing(spring);
  }
}

.drawer-content {
  transform: translateX(-16px);

  @include motion.reduced {
    transform: none;
  }
}

CSS variables ​

The same values sit on :root — for plain CSS, inline styles and Tailwind. The names follow the scale: --cds-motion-duration-<name>, --cds-motion-easing-<name>, --cds-motion-loop-<name> (names from the tables above). Switching the theme does not change them.

css
.toast {
  transition: opacity var(--cds-motion-duration-fast) var(--cds-motion-easing-enter);
}

The motion JS object ​

When a script drives the motion — a timer, the Web Animations API, a delay calculation — take the scale from motion. Durations and loops are numbers in milliseconds, curves are CSS strings.

ts
import { motion } from '@central-design-system/components';

motion.durations.fast; // 200
motion.easings.enter; // 'cubic-bezier(0.05, 0.7, 0.1, 1)'
motion.loops.pulse; // 2000

el.animate([{ opacity: 0 }, { opacity: 1 }], {
  duration: motion.durations.fast,
  easing: motion.easings.enter
});