Exploring the New Expressive Components in Material Design
Google has been shaping the language of digital design for nearly a decade. From the flat elegance of Material Design 1 to the adaptable Material You, each iteration has brought clarity and beauty to Android apps. With Material 3 Expressive, the next chapter unfolds — one that doesn’t just aim for consistency, but for vibrancy, emotion, and a sense of play.
Why “Expressive”?
The word expressive is intentional. This evolution of Material Design is about giving apps more personality. Instead of limiting designers to flat surfaces and strict grids, Expressive embraces color, motion, typography, and shape as tools to build deeper connections with users. In short, it’s about designing for delight, not just utility.
🔹 Button Groups
Buttons are the backbone of interactivity. But in many apps, actions live scattered across screens. Enter theButton Group, one of the newest Expressive components.
- Customizable Sizes: From XS to XL, buttons can match the tone of the screen they’re in.
- Shapes & Motion: Rounded corners, pill forms, and subtle animated feedback make them fun.
- Organized Actions: Grouping related buttons improves clarity and user flow.
Use Case: In an e‑commerce app, a button group could hold “Add to Cart,” “Buy Now,” and “Wishlist,” giving users multiple pathways without cluttering the screen.
Jetpack Compose Example
@Composable
fun ActionButtonGroup() {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(onClick = { /* TODO */ }
) { Text("Add to Cart") }
OutlinedButton(onClick = { /* TODO */ }
) { Text("Wishlist") }
Button(
onClick = { /* TODO */ },
colors = ButtonDefaults.buttonColors(containerColor = Color.Green)
) {
Text("Buy Now")
}
}
}
}
🔹 FAB Menu
Floating Action Buttons (FABs) have been iconic in Material Design since the beginning. With Expressive, the familiar FAB evolves into a FAB Menu — a more powerful, colorful, and context‑aware way to surface quick actions.
- No More Speed Dial: Instead of mini FABs, actions expand into a clear, menu‑like panel.
- Dynamic Colors: FAB Menus inherit Material You palettes, adapting to wallpapers or themes.
- Fluid Motion: Transitions feel organic — opening the menu feels like unfolding, not popping.
Use Case: In a note‑taking app, a FAB Menu could reveal actions like “New Note,” “Voice Memo,” and “Scan Document,” each styled with expressive color accents.
Jetpack Compose Example
@Composable
fun FabMenuDemo() {
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier.padding(16.dp)
.fillMaxSize()
.systemBarsPadding(),
contentAlignment = Alignment.BottomEnd
) {
Column(horizontalAlignment = Alignment.End) {
// Show action buttons only when expanded
if (expanded) {
ExtendedFloatingActionButton(
onClick = { expanded = false }
) {
Text("New Note")
}
Spacer(modifier = Modifier.height(8.dp))
ExtendedFloatingActionButton(
onClick = { expanded = false }
) {
Text("Voice Memo")
}
Spacer(modifier = Modifier.height(8.dp))
ExtendedFloatingActionButton(
onClick = { expanded = false }
) {
Text("Scan Document")
}
Spacer(modifier = Modifier.height(16.dp))
}
// Main toggle FAB (always visible)
FloatingActionButton(onClick = { expanded = !expanded }) {
Icon(
imageVector = if (expanded) Icons.Default.Close else Icons.Default.Add,
contentDescription = "Menu"
)
}
}
}
}
Design summary — what makes an “Expressive” loading indicator
- Colorful & adaptive: uses Material You / MaterialTheme colors (surface/primary/secondary) or dynamic palettes.
- Fluid motion: smooth easing, easing-in/out plus subtle scale/pulse rather than harsh spinning.
- Layered depth: describes the visual style (multiple layers rather than flat).
- Minimal & accessible: doesn’t rely only on motion — provides content descriptions and optional progress text.
- Configurable: size, stroke/thickness, color palette, and animation behaviors should be easy to change.
Jetpack Compose — expressive loading indicator (Kotlin)
- This composable implements:
- a rotating arc (indeterminate) with easing,
- an inner pulsing circle for depth,
- uses MaterialTheme colors (so it follows M3 dynamic palettes),
- accessibility content description and optional label.
@Composable
fun ExpressiveLoadingIndicator() {
val size = 56.dp
val strokeWidth = 4.dp
val primaryColor = MaterialTheme.colorScheme.primary
val accentColor = MaterialTheme.colorScheme.secondary
val glowColor = primaryColor.copy(alpha = 0.14f)
val density = LocalDensity.current
val strokePx = with(density) { strokeWidth.toPx() }
// Infinite rotation
val infiniteTransition = rememberInfiniteTransition(label = "loaderTransition")
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1600, easing = LinearOutSlowInEasing),
repeatMode = RepeatMode.Restart
), label = "rotation"
)
// Sweep progress
val sweepProgress by infiniteTransition.animateFloat(
initialValue = 0.25f,
targetValue = 0.85f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 900, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
), label = "sweep"
)
// Inner pulse
val pulse by infiniteTransition.animateFloat(
initialValue = 0.85f,
targetValue = 1.12f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 700, easing = EaseInOutCubic),
repeatMode = RepeatMode.Reverse
), label = "pulse"
)
Box(
modifier = Modifier.size(size),
contentAlignment = Alignment.Center
) {
// Glow layer
Canvas(modifier = Modifier.fillMaxSize()) {
val w = size.toPx()
val center = Offset(w / 2f, w / 2f)
val radius = (w / 2f) - strokePx * 1.5f
drawCircle(
brush = Brush.radialGradient(
colors = listOf(glowColor, Color.Transparent),
center = center,
radius = radius * 1.15f
),
radius = radius * 1.15f,
center = center,
alpha = 1f
)
}
// Rotating arc + pulsing dot
Canvas(
modifier = Modifier
.fillMaxSize()
.padding((strokeWidth * 1.2f))
) {
val w = size.toPx()
val center = Offset(w / 2f, w / 2f)
val radius = (w / 2f) - strokePx
val sweepAngle = (sweepProgress * 360f).coerceIn(8f, 320f)
val startAngle = rotation
val stroke = Stroke(width = strokePx, cap = StrokeCap.Round)
// Background track
drawArc(
color = primaryColor.copy(alpha = 0.11f),
startAngle = 0f,
sweepAngle = 360f,
useCenter = false,
topLeft = Offset(center.x - radius, center.y - radius),
size = Size(radius * 2f, radius * 2f),
style = stroke
)
// Gradient arc
val arcBrush = Brush.sweepGradient(listOf(primaryColor, accentColor, primaryColor))
drawArc(
brush = arcBrush,
startAngle = startAngle,
sweepAngle = sweepAngle,
useCenter = false,
topLeft = Offset(center.x - radius, center.y - radius),
size = Size(radius * 2f, radius * 2f),
style = stroke
)
// Pulsing dot
val dotRadius = radius * 0.16f * pulse
drawCircle(
color = accentColor,
radius = dotRadius,
center = Offset(
center.x + radius * 0.5f * cos(Math.toRadians(rotation.toDouble())).toFloat(),
center.y + radius * 0.5f * sin(Math.toRadians(rotation.toDouble())).toFloat()
),
alpha = 0.95f
)
}
}
}
How to use
@Composable
fun LoadingScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
ExpressiveLoadingIndicator()
}
}
@Composable
fun LoadingScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
ExpressiveLoadingIndicator()
}
}
OUTPUT:
Customization tips
Accessibility:
- Always provide contentDescription for screen readers.
- Avoid long, continuous motion for users who have motion sensitivity — respect the system setting AnimationScale if needed (you can check AnimationScale through Settings.Global or use LocalView accessibility flags).
- Provide alternative textual status where appropriate (e.g., “Loading content…”).
When to prefer this expressive indicator
- Splash screens, sync operations, background tasks where you want to add a little personality.
- Feature discovery or onboarding flows where a friendly motion increases delight.
- Not recommended for constantly visible micro-loaders in dense lists — where a compact, minimal spinner is better.
Beyond Components – The Expressive Philosophy
Expressive is more than just new UI widgets. It’s a philosophy that reshapes the foundation of Material Design:
- Vibrant Colors: Palettes now feel richer, going beyond pastel tones.
- Fluid Motion: Micro‑animations are bouncy, natural, and guide user attention.
- Adaptive Typography: Variable fonts like Roboto Flex make text adapt to screens seamlessly.
- Shapes & Containment: Bold containers emphasize hierarchy, helping users focus.
Expressive design is based on extensive research across thousands of users, ensuring accessibility remains a core priority alongside aesthetics.
Developer Takeaways
If you’re building with Jetpack Compose or Material 3 libraries, here’s what to know:
- Figma kits already support Expressive styles for faster prototyping.
- Compose Material APIs are gradually rolling out support for Button Groups and FAB Menus.
- Colors, motion specs, and typography updates can be applied globally via MaterialTheme.
- Start small — try replacing your FAB with a FAB Menu or grouping buttons in key flows.
Final Thoughts
Material Design has always been about marrying function with form. With Expressive, Google takes a bold step toward designs that feel human, joyful, and alive. Whether you’re refreshing an existing app or launching a new one, these components offer an opportunity to create experiences that connect emotionally with users.
The best part? You don’t have to wait — Expressive Material is rolling into Android 16 and Jetpack Compose, making it easier than ever to bring these vibrant experiences into your app.