Shape morphing with MaterialShapes in Jetpack Compose
Short answer: call .toShape() on any MaterialShapes polygon to use it as a normal Compose
Shape. To animate between two shapes, create a Morph(start, end), wrap it in a small Shape
that calls morph.toPath(progress), and animate progress with the theme’s motion scheme.
Static shapes
Section titled “Static shapes”@Composablefun CookieBadge() { Box( Modifier .size(64.dp) .background(MaterialTheme.colorScheme.primary, MaterialShapes.Cookie9Sided.toShape()), )}MaterialShapes polygons are normalized to a 0..1 box, so toShape() scales them to whatever
size the layout gives.
A morphing shape
Section titled “A morphing shape”Compose has no built-in “morphing shape”, but it takes very little code:
/** Draws [morph] at [progress] (0 = start polygon, 1 = end polygon), scaled to the layout size. */class MorphShape(private val morph: Morph, private val progress: Float) : Shape { override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline { val path = morph.toPath(progress = progress, path = Path()) path.transform(Matrix().apply { scale(size.width, size.height) }) return Outline.Generic(path) }}Then animate the progress. With MotionScheme.expressive() the spatial spec is a spring, so the
shape overshoots slightly, which is the Expressive feel:
@Composablefun MorphOnTap() { var selected by remember { mutableStateOf(false) } val morph = remember { Morph(MaterialShapes.Circle, MaterialShapes.Cookie9Sided) } val progress by animateFloatAsState( targetValue = if (selected) 1f else 0f, animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), // springy with MotionScheme.expressive() label = "morph", ) Box( Modifier .size(96.dp) .background(MaterialTheme.colorScheme.primary, MorphShape(morph, progress)) .clickable { selected = !selected }, )}In the paywalls
Section titled “In the paywalls”M3 Expressive Paywall uses this in two places: the selection radio morphs from a circle into
Cookie9Sided, and the plan picker spotlight card morphs
between a cookie, a sunny shape and a flower as you switch plans.


- Morph between shapes with similar complexity. Circle to cookie looks smooth; circle to heart can twist.
- Rotate the morphed shape slightly with
graphicsLayer { rotationZ = … }for more motion. - Respect reduced motion: with
MotionScheme.standard()the springs are calmer.
Related
Section titled “Related”FAQ
Which shapes are available?
MaterialShapes includes Circle, Square, Pill, Cookie4Sided to Cookie12Sided, Sunny, VerySunny, Flower, Clover4Leaf, Clover8Leaf, Burst, SoftBurst, Heart, Gem and more. They are RoundedPolygon values from androidx.graphics.shapes.
Do I need the graphics-shapes dependency?
material3 brings it transitively. Add androidx.graphics:graphics-shapes explicitly if you use Morph or RoundedPolygon directly, as this guide does.
Why does my morph restart on every recomposition?
The Morph or its polygons are created again on each recomposition. Wrap the Morph in remember, and keep target shapes as stable instances.
Try it in your app
Add the library (JitPack, minSdk 26), then follow Getting started.
implementation("com.github.sanikantv:m3-expressive-paywall:v1.0.0")⭐ If this guide helped, star M3 Expressive Paywall on GitHub. It helps other Compose developers find it.
Built by Sanikant. Set in Google Sans Flex (SIL Open Font License). Not affiliated with Google.