Skip to main content brightness_auto motion_photos_off palette download more_vert
toc

Motion

Motion makes a UI expressive and easy to follow: springs for physics, easing and duration tokens, and transition patterns.

Material 3 Expressive uses a physics-based motion system. Springs drive spatial motion (position, size, rotation, corner shape) and effects springs drive properties that must not overshoot, like color and opacity. One spring can serve transitions, button effects and gestures, which keeps the product feeling consistent. Each spring is exposed as a CSS linear() easing with a matching duration, so a transition can use it directly.

  • Expressive is the default. Use it for most situations, especially hero moments and key interactions. Spatial springs overshoot and bounce into place.
  • Standard moves with minimal bounce and suits utilitarian products. Apply it to a subtree with the md-motion-standard class.

The spec's three levels of customization map to CSS like this:

  1. Use a default scheme: do nothing (Expressive), or add md-motion-standard.
  2. Create a custom scheme: override the spring custom properties in your own :root rule.
  3. Swap the scheme for one element: put md-motion-standard on that element.
Save
<div class="md-motion-standard">
  <md-gb-button color="filled">Save</md-gb-button>
</div>

Spring tokens follow --md-sys-motion-spring-<speed>-<spatial|effects>, with fast, default and slow speeds. Each has a -duration token:

HTMLCSSJS
<md-gb-button id="slide" color="filled">Slide</md-gb-button>
<div id="panel" class="panel">Panel</div>
Spring Expressive Standard
Spatial fast / default / slow 350 / 420 / 570ms 160 / 220 / 340ms
Effects fast / default / slow 140 / 220 / 300ms 140 / 220 / 300ms

Springs handle interruption and retargeting well, so prefer them. For animations without interruptions or gestures you can use the curve fallback: --md-sys-motion-curve-<speed>-<spatial|effects> (a cubic-bezier()) with --md-sys-motion-curve-<speed>-<spatial|effects>-duration. They carry the values from the spec's web conversion table.

Reduced motion is on when the platform asks for it (prefers-reduced-motion: reduce) or inside an element with the md-motion-reduced class (spatial motion is off there, fades stay). The spatial springs then become instant (0ms) and the effects springs keep their timing. People still see fades and color changes, but not sliding, scaling, parallax or shape morphing.

Do not build your own motion gate. Read the durations from the tokens so the scheme and reduced motion apply without extra code.

A gesture has to keep its momentum: a fling that lets go at 2000px/s must not restart from rest, and a settle that is aimed at a new target half way must not jump. A CSS transition cannot do either, so labs/gb also has the same springs as physics, in labs/gb/components/shared/spring.ts. readSpring() returns the damping ratio and stiffness of a spring token (Expressive or Standard, whichever applies to the element), and Spring solves it in closed form and samples it every animation frame:

HTMLCSSTS
<md-gb-button id="fling" color="filled">Fling</md-gb-button>
<div id="panel" class="panel">Panel</div>

Under reduced motion the spring jumps to its target and rests at once. DragSettleController (the drag-and-settle behavior behind swipe to reveal, drawers, sheets and pane handles) does this for you when you pass onFrame: the settle starts at the release position with the release velocity, and the surface follows the frames with no CSS transition on the moved property. Without onFrame it keeps the CSS transition of the spring token.

Buttons and icon buttons (md-gb-button, md-gb-icon-button) morph their shape when pressed and when selected. That is spatial motion, so reduced motion turns it off and keeps the color change.

Components that script their motion use the helpers in labs/gb/components/shared/motion.ts:

  • springDuration(element, speed, kind) returns the duration of a spring in milliseconds, already 0 for spatial springs when motion is reduced.
  • isReducedMotion(element) and onReducedMotionChange(...) follow the setting, including inside .md-motion-reduced.
  • waitForMotion(...) resolves when the animations end or when its timeout runs out, so waiting never hangs. waitForDuration(ms, signal) is the plain timed wait.
  • The spec's Compose APIs (MotionScheme objects, a CompositionLocal per element) have no web equivalent. The CSS steps above replace them.
  • Springs are pre-sampled into linear() easings for transitions. A transition cannot take a starting velocity, so gestures use the solver described in Gestures instead.

M3 Expressive moves components to springs, but transitions still use the easing and duration system, and so can products that have not migrated yet. The spec offers a starting pair for each case:

Easing token Duration token Use it to
emphasized long2 (500ms) Begin and end on screen
emphasized-decelerate medium4 (400ms) Enter the screen
emphasized-accelerate short4 (200ms) Exit the screen
standard medium2 (300ms) Begin and end on screen, small utility transitions
standard-decelerate medium1 (250ms) Enter the screen
standard-accelerate short4 (200ms) Exit the screen
HTMLCSSJS
<md-gb-button id="enter" color="filled">Enter</md-gb-button>
<div id="sheet" class="sheet">Sheet</div>

Tokens are --md-sys-motion-easing-<set>[-accelerate|-decelerate] and --md-sys-motion-duration-<short|medium|long|extra-long><1-4>. Short is 50 to 200ms, medium 250 to 400ms, long 450 to 600ms and extra long 700 to 1000ms, in steps of 50ms (extra long in steps of 100ms).

How to choose:

  • Easing set. Emphasized for most transitions, with its decelerate and accelerate variants for entering and exiting. Standard for small, quick, utility transitions.
  • Easing type. Plain for elements that begin and end on screen, decelerate when entering, accelerate when leaving for good. An element that leaves temporarily (a drawer that comes back) uses the plain curve.
  • Duration. Bigger transitions and larger distances take longer. Exits are shorter than enters, because the user's attention has already moved on.

Reduced motion does not change these tokens. Gate sliding and scaling yourself (the transition helpers below do) or use the spring tokens, which follow the setting.

Not covered: the spec's emphasized easing is a path curve. On the web --md-sys-motion-easing-emphasized is the single cubic-bezier(0.2, 0, 0, 1) approximation, so it is equal to standard.

A transition is a short animation that connects elements or full-screen views and shows how they relate. The spec defines six patterns and notes that they still use the easing and duration tokens above, not springs. labs/gb implements them in styles/motion/transitions/ with the View Transitions API, and falls back to the Web Animations API when the browser lacks it (or when you pass viewTransition: false).

The pattern styles are part of m3Styles (see Quick Start). Adopt them on the document, because ::view-transition-* rules only work there. The helpers come from @material/web/labs/gb/styles/motion/transitions/transitions.js.

Every helper takes update, a function that changes the DOM to the new state, and returns a promise that resolves when the motion ends. Common options: reducedMotion, viewTransition, duration and easing. The timings come from --md-transition-* custom properties (for example --md-transition-shared-axis-duration), so you can restyle a pattern in CSS.

For an element that becomes a new surface and shows more detail: a card that opens a details page, a FAB that opens a sheet, a search box, a list item. The container's bounds, corner radius and color morph while its content fades through (Emphasized, 500ms by default). from and to can be the same element or different ones. Swap them to collapse back.

HTMLCSSTS
<div id="card" class="surface card">
  Inbox
  <md-gb-button id="open">Open</md-gb-button>
</div>
<div id="detail" class="surface detail" hidden>
  Inbox details
  <md-gb-button id="close">Close</md-gb-button>
</div>

For screens at consecutive levels of hierarchy, like an inbox and a message. The screens slide along the x axis as they fade (axis: 'y' and 'z' are available too). Always pair forward with backward when the user goes back, so the direction stays consistent.

HTMLCSSTS
<div id="stage" class="stage">
  <section id="inbox">
    <p>Inbox</p>
    <md-gb-button id="forward" color="filled">Open message</md-gb-button>
  </section>
  <section id="thread" hidden>
    <p>Message</p>
    <md-gb-button id="backward">Back</md-gb-button>
  </section>
</div>

scope limits the motion to one region. Without it the whole page animates.

For peers at the same level, like tabs or the photos in an album. Both pages slide the full width together. The direction is next or previous and follows the writing direction (RTL is handled).

HTMLCSSTS
<md-gb-button id="next" color="tonal">Next photo</md-gb-button>
<div id="panels" class="panels">
  <p id="photo">Photo 1</p>
</div>

For switching between top-level destinations (navigation bar, rail or drawer). The old screen quickly fades out, then the new one fades in, so they never overlap. These destinations are not related spatially, so there is no directional slide; the new screen only scales up slightly as it fades in.

HTMLCSSTS
<md-gb-button id="library" color="tonal">Library</md-gb-button>
<main id="main" class="main">
  <p id="title">Home</p>
</main>

For introducing or removing a component. enter(element, options) shows it (hidden = false) and exit(element, options) animates it away, then you hide or remove it. Both resolve when the animation ends.

  • Within screen bounds (menus, dialogs, snackbars, tooltips, FABs). The default style: 'expand' is the Android style: the component expands away from the nearest screen edge, without scale. style: 'scale' is the iOS style (scale in, fade out). Pick the edge with origin.
  • Beyond screen bounds (drawers, sheets, banners, navigation). style: 'slide' slides the component on and off from its edge. Set temporary: true for something that comes back, like a drawer, so it exits with the plain Emphasized curve instead of the accelerate one.
HTMLCSSTS
<md-gb-button id="toggle-menu" color="tonal">Menu</md-gb-button>
<md-gb-button id="toggle-drawer" color="tonal">Drawer</md-gb-button>
<ul id="menu" class="panel" hidden>
  <li>Copy</li>
  <li>Paste</li>
</ul>
<nav id="drawer" class="panel drawer" aria-label="Sections" hidden>Inbox</nav>

Match the origin to where the component lives: a notification from the top, a drawer from the start edge, a bottom sheet and the keyboard from the bottom. In templates, enterExitClasses(state, options) returns a classMap() object (md-transition-enter, md-transition-from-top and so on), and clearEnterExit(element) removes the classes.

Hide and reveal while scrolling

Link to “Hide and reveal while scrolling”

A component can also leave and return based on a scroll gesture, to give more room for browsing: a top app bar or a navigation bar that slides off the screen edge while the user scrolls down and back on when they scroll up. scrollEnterExit(element, options) does this with the slide enter and exit motion and returns a handle with hidden, show(), hide() and destroy().

HTMLCSSTS
<div class="frame">
  <header id="top-bar" class="top-bar">Inbox</header>
  <div id="content" class="content">
    <p>Scroll down to hide the bar and up to bring it back.</p>
    <p>Message from Ana</p>
    <p>Message from Ben</p>
    <p>Message from Chen</p>
    <p>Message from Dana</p>
    <p>Message from Eli</p>
    <p>Message from Fay</p>
    <p>Message from Gus</p>
    <p>Message from Hana</p>
  </div>
</div>

target is the scrolling element or its id (the window by default), origin the edge it slides toward. It uses the same scroll rules as the app bar and navigation bar, so they all behave alike: it reveals at the top and when scrolled up, hides after a short scroll down, never hides while focus is inside it, and reveals when focus moves into it. A hidden element is inert. With reduced motion it fades instead of sliding. Call destroy() when the element goes away. Components that already have scroll-behavior="hide" do not need this.

A coplanar component sits at the same elevation as the main content (a standard side sheet), so the content shrinks to make room as it enters and grows back as it exits. coplanar(element, open, options) animates the element's size from zero to its natural size with the enter motion (and back with the exit motion), so a flex or grid neighbor resizes with it. It shows the element when opening and sets hidden when closing ends.

HTMLCSSTS
<md-gb-button id="toggle" color="tonal">Toggle panel</md-gb-button>
<div class="layout">
  <main class="main">Main content</main>
  <aside id="panel" class="panel" hidden>Side panel</aside>
</div>

axis: 'block' grows the element along the block axis instead of the inline axis. With reduced motion the size changes at once and only a fade plays. md-gb-side-sheet already does this itself: a standard (not modal) sheet takes its width as open is set, so the content beside it shrinks with its spring motion. Use coplanar() for your own panels.

For the step from a loading state to loaded content. The skeleton hints at where content will appear and holds its space, so the layout does not shift. It pulses subtly from the top left to the bottom right, and the content fades in on top when it is ready.

HTMLTS
<md-gb-skeleton loading shape="text"></md-gb-skeleton>
<md-gb-skeleton id="card" loading>
  <p>Content that loads.</p>
</md-gb-skeleton>
<md-gb-button id="done" color="tonal">Finish loading</md-gb-button>

shape is rect, text or circle. While loading the element sets aria-busy="true" and makes its content inert. For markup without the element, use the classes md-skeleton, md-skeleton-text and md-skeleton-circle.

Accessibility and good practice

Link to “Accessibility and good practice”

The spec asks transitions to follow accessibility settings, stay consistent, keep layouts stable, avoid jump cuts, use one spatial model and direction, fade cleanly and stay simple.

  • Every helper follows prefers-reduced-motion and replaces sliding and scaling with a quick fade. Pass reducedMotion: true to force it, as an app setting would.
  • Fade through never overlaps the old and new content. The content fade of a container transform is timed to the fastest part of the motion.
  • Use one pattern per kind of navigation, and keep forward and backward mirrored.
  • Dialogs and similar components that appear in place use the short fade (--md-transition-enter-fade-duration).
  • Keep most of a product on one scheme. Switch schemes only for a few key moments, and do it on purpose.
  • Do not use motion to carry meaning on its own. Reduced motion removes it, so pair it with a change people can see without motion.
Install Material Web 3.0

Your browser did not offer to install the docs as an app. Install them from the browser instead:

Close
Send feedback
Bug Documentation Idea

Opens a new GitHub issue for material-components/material-web in a new tab, with this filled in.

Cancel Open issue