These classes transition your content with only CSS. Each transition has a base class that applies the styles it needs, plus state classes that control the transition.
Scale
Add the base class scale-transition to scale an element in and out. Then add
scale-out to scale the element down until it is hidden.
To start an element hidden, add scale-out first. Then add
scale-in to scale the element up until it shows.
With reduced motion requested, these state changes happen at once.
Scaling out only hides the element visually. When you use it for disclosure, move focus and manage hidden controls yourself.
<!-- Scaled in -->
<button id="scale-demo" type="button" class="button extra circle scale-transition" aria-label="Add"><span class="material-symbols" aria-hidden="true">add</span></button>
<!-- Scaled out -->
<button id="scale-demo" type="button" class="button extra circle scale-transition scale-out" aria-label="Add"><span class="material-symbols" aria-hidden="true">add</span></button>
Motion tokens
Components take their easing curves and on-scale durations from the Material 3
md.sys.motion tokens. Override one on :root to change every
component that uses it, or on a subtree to change only what is inside it. Off-scale times,
such as the 2.1s progress loop, stay literal.
| Token | Value |
|---|---|
--md-sys-motion-easing-standard | cubic-bezier(0.2, 0, 0, 1) |
--md-sys-motion-easing-standard-accelerate | cubic-bezier(0.3, 0, 1, 1) |
--md-sys-motion-easing-standard-decelerate | cubic-bezier(0, 0, 0, 1) |
--md-sys-motion-easing-emphasized | cubic-bezier(0.2, 0, 0, 1) |
--md-sys-motion-easing-emphasized-accelerate | cubic-bezier(0.3, 0, 0.8, 0.15) |
--md-sys-motion-easing-emphasized-decelerate | cubic-bezier(0.05, 0.7, 0.1, 1) |
--md-sys-motion-easing-linear | cubic-bezier(0, 0, 1, 1) |
--md-sys-motion-duration-short1 to short4 | 50, 100, 150, 200ms |
--md-sys-motion-duration-medium1 to medium4 | 250, 300, 350, 400ms |
--md-sys-motion-duration-long1 to long4 | 450, 500, 550, 600ms |
--md-sys-motion-duration-extra-long1 to extra-long4 | 700, 800, 900, 1000ms |
CSS cannot draw Material's emphasized path, so emphasized uses the standard
curve, as Material's own web components do.
When the user asks for reduced motion, every duration token and every spring -duration token becomes 0s. Each component that reads them then changes state without moving. The override has no !important, so a page can opt an element back in. Off-scale literal times are not tokens. Their components cancel them in their own reduced-motion rules.
<style>
/* Every 200ms transition runs at 150ms instead. */
:root { --md-sys-motion-duration-short4: 150ms; }
</style>
Springs
Material 3 Expressive describes motion as springs: a damping ratio and a stiffness. CSS has no spring timing function. The framework solves each spring when it compiles the sheet and samples it into a linear() easing. That easing runs for the time the spring takes to settle. Every spring has an easing token and a -duration token. Use the two together.
| Token | Standard (damping, stiffness) | Expressive |
|---|---|---|
--md-sys-motion-spring-fast-spatial | 0.9, 1400 | 0.6, 800 |
--md-sys-motion-spring-default-spatial | 0.9, 700 | 0.8, 380 |
--md-sys-motion-spring-slow-spatial | 0.9, 300 | 0.8, 200 |
--md-sys-motion-spring-fast-effects | 1, 3800 | same |
--md-sys-motion-spring-default-effects | 1, 1600 | same |
--md-sys-motion-spring-slow-effects | 1, 800 | same |
Set motion="expressive" on the root or on any subtree to switch the spatial springs to the bouncier expressive ones. Set motion="standard" to switch a subtree back. The standard motion scheme is the default. Effects springs, for color and opacity, never overshoot and are the same in both schemes.
<style>
.panel {
transition: translate var(--md-sys-motion-spring-default-spatial-duration)
var(--md-sys-motion-spring-default-spatial);
}
</style>
<main motion="expressive">…</main>
In Sass, spring($damping, $stiffness) and spring-duration($damping, $stiffness) turn any spring into the same pair.
.drawer {
transition: translate spring-duration(0.8, 380) spring(0.8, 380);
}
A linear() easing always starts from rest, so a transition interrupted part way restarts without the speed it had. This shows most on a dragged bottom or side sheet. So the sheet solves its spring when the user releases it instead. It springs back from where the finger left it, at the finger's speed, and a press while it moves catches it where it is. If a page overrides a sheet's motion tokens, the sheet keeps the CSS transition they ask for.
Each spring also has -damping and -stiffness tokens that hold its two numbers and follow motion. Scripts that solve a spring themselves can read them.