Skip to content

How to make a Compose screen expressive

Short answer: find the one action and the one piece of information that matter most on the screen. Make them stand out with shape, color and emphasized type. Group the rest. Turn selections into shape changes, and animate with springs. Then stop.

Google’s research behind M3 Expressive found people spotted key UI elements up to four times faster on expressive screens. That only works if one thing stands out.

  1. Find the primary action and the key fact. One of each.
  2. Make the action bigger, with a contrasting shape and strong color. For example, a medium button, or a square-ish FAB next to a pill toolbar.
  3. Emphasize the key fact. Use titleLargeEmphasized or displaySmallEmphasized, and save the huge display styles for hero screens.
  4. Group the rest in surfaceContainer* containers. The most important group gets the brightest one.
  5. Turn selection into shape: connected ToggleButtons, SegmentedListItem selected states.
  6. Morph instead of swapping, and use MaterialTheme.motionScheme springs.
  7. Stop and test: light, dark, two dynamic color wallpapers, font scale 200%.
Step 2: a pill toolbar and a contrasting FAB
/**
* Google's before/after: a docked bottom bar with a FAB becomes a floating toolbar plus a
* separate FAB in a contrasting shape and color. The FAB is the primary action, so it gets the
* vibrant container and the square-ish shape; the toolbar stays a pill.
*/
@Composable
fun ComposeActions(actions: List<Pair<ImageVector, String>>, primary: ImageVector, onPrimary: () -> Unit) {
HorizontalFloatingToolbar(
expanded = true,
floatingActionButton = {
FloatingToolbarDefaults.VibrantFloatingActionButton(onClick = onPrimary) {
Icon(primary, contentDescription = "Compose")
}
},
) {
actions.forEach { (icon, label) ->
IconButton(onClick = {}) { Icon(icon, contentDescription = label) }
}
}
}

Google’s guidance is one or two hero moments per product, not per screen. Examples are a paywall, an onboarding finish or a “goal reached” screen. Everywhere else, keep it calm.

A small hero detail: Sunny badges only on goal days
/**
* Shapes as highlights in data, like Google's steps chart: plain pill bars, and a Sunny-shaped
* check badge only on the days that hit the goal. The badge is decoration, so the bar still says
* what it means (contentDescription on the column, not on the shape).
*/
@Composable
fun WeekChart(steps: List<Int>, goal: Int, check: ImageVector, max: Int = 15_000) {
Row(
modifier = Modifier.height(200.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.Bottom,
) {
steps.forEach { value ->
val hit = value >= goal
Column(
modifier = Modifier.width(40.dp).fillMaxHeight(),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.width(40.dp)
.height((200 * value / max).coerceAtLeast(40).dp)
.background(
if (hit) MaterialTheme.colorScheme.tertiaryContainer else MaterialTheme.colorScheme.primary,
CircleShape,
),
contentAlignment = Alignment.TopCenter,
) {
if (hit) {
Box(
Modifier
.size(36.dp)
.background(MaterialTheme.colorScheme.tertiary, MaterialShapes.Sunny.toShape()),
contentAlignment = Alignment.Center,
) {
Icon(check, contentDescription = null, tint = MaterialTheme.colorScheme.onTertiary)
}
}
}
}
}
}
}

The full design rules →

Want your agent to get this right every time? Install the skill. If this page helped,star the repo on GitHub.

Built by Sanikant. Set in Google Sans Flex (SIL Open Font License), icons from Material Symbols (Apache 2.0). Material Design is a Google trademark; this project is not affiliated with Google.