Motion UI and animation
Use Motion UI's transition classes, write custom animations with its mixins, and keep motion respectful of reduced-motion preferences.
Transition and animation classes
Motion UI is a small library of keyframes and transition helpers, distributed separately from Foundation. Its classes work on any element, but they are most useful where a Foundation component accepts an animation name — reveal, off-canvas and toggler all take them.
/* app.scss */
@import "settings";
@import "motion-ui";
/* Two switches: transitions are the in/out effects, animations are the
looping or one-shot keyframe effects. Import only what you use. */
@include motion-ui-transitions;
@include motion-ui-animations;
/* Or generate a specific set to keep the CSS small: */
// @include mui-series(...);<!-- transition on a toggler -->
<div data-toggler data-animate="slide-in-down slide-out-up" class="callout">
<p>Content that slides in and out.</p>
</div>
<!-- animation on any element -->
<div class="animated wobble">Attention</div>
<div class="animated fade-in fast">Fast fade</div>
<div class="animated slide-in-right slow">Slow slide</div>
<!-- a reveal driven by Motion UI names -->
<div class="reveal" id="confirm" data-reveal
data-animation-in="slide-in-down fast" data-animation-out="slide-out-up fast">
<h2 class="h5">Confirm deletion</h2>
<button class="button" data-close>Cancel</button>
</div>| Category | Examples | Applied by |
|---|---|---|
| Slide | slide-in-down, slide-out-left | Class, or the data-animate attribute |
| Fade | fade-in, fade-out | Same |
| Zoom | zoom-in, zoom-out | Same |
| Hinge and spin | hinge-in-from-top, spin-cw | Same |
| Speed modifier | fast, slow | Appended to the effect name |
| Easing modifier | linear, ease | Appended after the speed |
💡
Motion UI classes animate with CSS
transform and opacity, which the compositor can handle without layout work. Avoid animating width, height or top on a large element — those trigger layout on every frame and are the usual cause of janky motion.Custom animations with the mixins
/* A one-off keyframe animation defined in your own Sass */
.my-notification-enter {
@include mui-animation(slide(up, 20px), fade());
}
/* Register a reusable named animation */
@include mui-animation(
$name: 'card-flip',
$animation: (perspective(800px), rotateY(90deg) fade()),
$duration: 400ms,
$timing: ease-out,
$delay: 0s,
$iteration: 1,
$direction: normal,
$fill-mode: both
);
/* Use it like any other Motion UI animation */
.card.is-flipping {
@include mui-animation(card-flip);
}
/* Transition helper for hover states you control yourself */
.panel-hover {
@include mui-transition(transform 200ms ease-out, box-shadow 200ms ease-out);
&:hover { transform: translateY(-2px); box-shadow: 0 6px 18px rgb(0 0 0 / 0.08); }
}// Sequence animations without a timeline library: mui-series emits the classes
// with staggered delays, so the CSS does the scheduling.
// In markup, add the series wrapper and let the delays play out:
// <ul class="mui-series"> ... </ul>
// If you need real control, use the animationend event.
const card = document.querySelector('.card');
card.addEventListener('animationend', (event) => {
if (event.animationName === 'card-flip') card.classList.remove('is-flipping');
}, { once: true });Reduced motion and when not to animate
/* Motion UI ships a reduced-motion guard, but your own animations need one too.
This is the pattern: keep the state change, drop the movement. */
@media (prefers-reduced-motion: reduce) {
.animated,
.is-flipping,
[data-animate] {
animation: none !important;
transition: none !important;
}
}
/* If the animation carried meaning, replace it with an instant but visible
change — a border, a colour, an icon — rather than deleting it silently. */
@media (prefers-reduced-motion: reduce) {
.panel-hover:hover {
transform: none;
outline: 2px solid var(--primary);
}
}- Animation is feedback, not decoration. If removing it makes the interface confusing, the animation was load-bearing and needs a non-motion equivalent.
- Keep interactive transitions under about 200ms. Longer feels sluggish when the user is waiting for a response to their click.
- Never animate something the user must read while it moves — a moving paragraph is hostile to anyone with a reading difficulty.
- A single element moving is fine; a page of staggered entrances every time a route changes gets tiring by the third visit.
| Situation | Motion | Reduced-motion fallback |
|---|---|---|
| Panel expanding | Height or slide | Instant show with a visible state change |
| Notification arriving | Slide plus fade | Instant appear, keep the colour |
| Loading indicator | Continuous spin | Static label such as "Loading" |
| Decorative entrance | Staggered fade | Nothing |
| Error shake | Shake | Colour and icon change only |
FAQ
Do I need Motion UI if I use CSS animations?
No. It is a convenience library of pre-built keyframes and mixins. If your project has a design system with its own motion tokens, skip it and write the few keyframes you need — you will ship less CSS.
Why does my animation not run a second time?
Re-adding a class that is already present does not restart a CSS animation. Remove the class, force a reflow with
void element.offsetWidth, then add it again — or use animationend to clean up as shown above.Related
Interactive plugins: reveals, tabs, accordions and dropdowns Accessibility and RTL support
Last refreshed 2026-09-18.