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
fastSpatialSmall parts and direct feedback: handles, indicators, shape morphsmd-sys-motion-fast-spatial
spatialComponent containers: dialogs, sheets, navigation contentmd-sys-motion-spatial
slowSpatialLarge and full-screen surfacesmd-sys-motion-slow-spatial
fastEffectsHover, press and focus color; exitsmd-sys-motion-fast-effects
effectsColor and opacity on containers; fadesmd-sys-motion-effects
slowEffectsLarge-surface fadesmd-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 screenEnter and exitpresence(() => open, enterExit.fade)
An element expands into its own detail (a hero moment)Container transformcontainerTransform(update, { from, to })
Parent to child, or the next stepForward and backwardsharedAxis(update, { axis, direction })
Peers in one set: tabs, pages of a carouselLaterallateral(update, { direction })
Unrelated destinations from a nav bar or railTop levelfadeThrough(update)
Content is loadingSkeleton{@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.