What Material 3 Expressive actually changes in Compose

“Material You”, “Material 3”, “Material 3 Expressive” get used interchangeably, including by people shipping apps, and as a result plenty of apps advertising the third are doing the first. Dynamic colour arrived with Android 12 and has nothing to do with the 2025 revision. If an app’s expressive credentials are a wallpaper-derived accent colour, it is a Material 3 app with dynamic colour switched on.

I built a calendar on the expressive system. The colour part turned out to be the least of it, the motion part most of it, and two of the defaults are wrong for anything page-sized.

The motion scheme is the actual API

The change you can point at in code is MaterialTheme.motionScheme. It hands you animation specs rather than leaving every component to invent its own duration and easing:

@Composable
fun rememberSlideSpec(): FiniteAnimationSpec<IntOffset> =
    MaterialTheme.motionScheme.defaultSpatialSpec()

The specs come in two families, and the split is worth internalising. Both are springs, which I had wrong for months:

In the tokens the split comes down to one number: in the standard scheme every spatial spring has a damping ratio of 0.9 and every effects spring has 1.0.

It sounds fussy until you break it. A shared-axis transition where the fade is driven by the spatial spring shimmers at the settle, because an underdamped spring drives alpha past 1.0 and oscillates back. Material says this outright: effects springs exist for properties “where the property’s value should not be overshot”, and gives the same example: a background alpha shouldn’t bounce above 100%. Run the position on defaultSpatialSpec() and the opacity on defaultEffectsSpec() and it stops.

Getting them out of a composable scope is a small annoyance worth mentioning: AnimatedContent’s transitionSpec lambda is not composable, so you read the specs in a remember-style helper first and capture them.

Three speeds, and I still picked the wrong one first

Each family comes in fast, default and slow. Material ties them to how much of the screen moves: fast for “small components like switches and buttons”, default for things that “partially cover the screen like a bottom sheet or nav drawer”, slow for “full screen animations”.

I reached for fastSpatialSpec() to page the calendar between months, which is a full-screen content swap, and the settle read as a twitch rather than a glide. By Material’s rule that was two steps off: a full-screen swap is the slow case.

What I actually shipped is defaultSpatialSpec(), one step up rather than two. Paging a calendar is a thing people do repeatedly and fast, and the slow spring made it feel like the app was thinking about it. So this is taste overriding the guidance, and I’m flagging it as taste. If you want the documented answer for a full-screen transition, it is slow.

The expressive scheme is not automatically the right scheme

There are two motion schemes, standard and expressive, and the difference between them is entirely spatial. Their effects tokens are identical, damping 1.0 at the same three stiffnesses. What expressive changes is the bounce: spatial damping drops from 0.9 to 0.8, and its fast spring to 0.6.

For a calendar I went with standard. A month grid that bounces when it lands reads as sloppy, because a grid has hard edges that make you see exactly how far past the target it went. Note that 0.9 is still underdamped, so standard means less overshoot, not none. Material describes it as “a linear motion feel” rather than as a spring that never passes its mark.

Before you congratulate yourself on the restrained choice, as I briefly did: standard() is already Compose’s default. You only get the expressive scheme by opting into MaterialExpressiveTheme, so picking standard costs nothing and amounts to declining an upgrade.

It is an alpha, and that is the real cost

I had a section here about the opt-in burden, and checking it for this post is how I found out it was obsolete. MotionScheme, the six spec functions and MaterialTheme.motionScheme carry no experimental annotation at all in the version I ship. The only @ExperimentalMaterial3ExpressiveApi in that file sits on a deprecated LocalMotionScheme I never touch.

My app still has seven @OptIn(ExperimentalMaterial3ExpressiveApi::class) annotations. Every one of them is now a no-op I have been carrying around because the compiler does not complain about opting in to something that no longer needs it. They are on the list to delete.

The version is the real cost, and it hasn’t moved: this is androidx.compose.material3:1.5.0-alpha26, because the expressive APIs live only in the 1.5 alpha line. A dependency bump on an alpha is never routine: signatures move, things get renamed, and occasionally a component you built a screen around changes shape. What has actually helped is keeping the expressive surface small: the specs are read in a handful of helpers and the screens call those, so a rename costs a few files instead of a few dozen. Three of my screens read motionScheme directly, and those are the ones I expect to pay for.

Springs raise the stakes on reduce-motion

Spring physics is exactly the kind of motion that makes some people feel ill, and the system setting for it is not optional to respect. Where a transition checks it, it falls back either to a cross-fade on the effects spec or to snap() (no animation at all), depending on whether a fade would smear the thing being replaced.

Build this in from the first animation rather than adding it later. Retrofitting a reduce-motion path across a finished app means finding every animation you wrote, and you will not find them all. I know that because I wrote the confident version of this paragraph first, then went looking: my floating action button scales in, my search and detail overlays slide across the full screen, and an import badge springs in on DampingRatioMediumBouncy, none of them checking anything. Three that I found, in an app whose author was about to tell you he had this handled.

What I did not do

The revision also brings a wider type scale and more shape variance. Calendula leaves the type scale at Typography() and has never tuned it. It does let you pick a typeface (three bundled faces plus a file you import), but that swaps the family inside the default scale rather than reshaping it.

If you assume the default is the pre-expressive one, note that in material3 1.5 the default Typography() already carries the emphasized styles. Leaving it alone is not opting out of the wider scale. It is declining to customise it, which is a smaller claim than “built on M3 Expressive throughout” might suggest, and I would rather say so.

I’d apply the same caveat to the whole category. The system is worth adopting for the motion alone; a product claiming expressive design because it picked up your wallpaper colour is claiming something else entirely.

The app this came out of is Calendula. The source is on Codeberg, and what Glance does to all of this on the home screen is its own unhappy story.