Motion
Every animation runs on M3 Expressive springs, and every transition follows one of M3's six patterns.
Two rules
Springs only. There are no millisecond durations or easing curves. Every animation, in CSS or JS, uses one of six spring tokens.
No Svelte transitions. Mounting, unmounting and navigation run on Motion's springs through the library's helpers, which can be interrupted mid-flight and keep their velocity. Hover, press and selection color changes stay CSS transitions on the effects springs.
Springs
Spatial springs move, resize or reshape things and overshoot slightly. Effects springs change color or opacity and never overshoot. Don't animate opacity on a spatial spring.
| Token | Use for | CSS utility |
|---|---|---|
fastSpatial | Small parts and direct feedback: handles, indicators, shape morphs | md-sys-motion-fast-spatial |
spatial | Component containers: dialogs, sheets, navigation content | md-sys-motion-spatial |
slowSpatial | Large and full-screen surfaces | md-sys-motion-slow-spatial |
fastEffects | Hover, press and focus color; exits | md-sys-motion-fast-effects |
effects | Color and opacity on containers; fades | md-sys-motion-effects |
slowEffects | Large-surface fades | md-sys-motion-slow-effects |
Choosing a transition
Pick the pattern from how the two states relate (M3: applying transitions):
| The change | Pattern | Helper |
|---|---|---|
| A component appears on the current screen | Enter and exit | presence(() => open, enterExit.fade) |
| An element expands into its own detail (a hero moment) | Container transform | containerTransform(update, { from, to }) |
| Parent to child, or the next step | Forward and backward | sharedAxis(update, { axis, direction }) |
| Peers in one set: tabs, pages of a carousel | Lateral | lateral(update, { direction }) |
| Unrelated destinations from a nav bar or rail | Top level | fadeThrough(update) |
| Content is loading | Skeleton | {@attach skeleton} |
Keep one pattern per kind of change across the app, don't use bouncy springs for navigation, and fade old content out before new content fades in. Each pattern has a live demo in Storybook.
With a target, lateral clips the slide to the target's box, so peers never
leak across the page. That also cuts off a card's shadow or a focus ring drawn outside the content's
box until the slide ends. Pass crop: false to turn the clip off, or pad the target
and offset it with an equal negative margin.
Built into components
Components that own both states animate themselves. Dialogs, sheets, menus, popovers,
tooltips, selects and snackbars enter and exit; TabHolder panels slide laterally; the
floating action button morphs into its menu. Changes whose content lives in your app, such as route
changes, card-to-detail and wizard steps, are yours to animate with the helpers above.
Route changes
Pages reached from a Rail or Navbar are top-level destinations, so they
fade through. In SvelteKit, hook the helper into onNavigate in your root layout:
<script lang="ts">
import { onNavigate } from '$app/navigation';
import { fadeThrough } from '@noxlovette/material';
onNavigate((navigation) => {
// A hash change on the same page isn't a new destination.
if (navigation.from?.url.pathname === navigation.to?.url.pathname) return;
return new Promise((resolve) => {
fadeThrough(async () => {
resolve();
await navigation.complete;
}, { target: 'main' });
});
});
</script> Fade only the region whose content changed. When a nested nav (a guides list, a settings
drawer) stays on screen, target that section's content pane between its own pages, and main only when the destination in the rail or navbar changes. This site does exactly
that in its root layout.
For hierarchy (a list to its item), use sharedAxis with direction: 'forward' going in and 'backward' coming out.