Material Design 3 for Android: Dynamic Color, Typography, and the New Design Language
Your app ships with a clean blue primary color, carefully chosen elevation shadows, and a typography scale your designer agonized over. Then a user opens it on Android 12 next to their wallpaper — a deep terracotta sunset — and your app looks like it landed from a different planet.
That mismatch is what Material Design 3 was built to solve. Not just visually, but architecturally. MD3 isn’t a reskin of MD2. It’s a rethinking of how color, type, and motion should adapt to the person holding the phone — not just the brand that shipped the app.
If you’re still writing MaterialTheme with primaryColor = Color.Blue, you’re already behind. Here’s what changed and how to catch up.
What Actually Changed (The Parts That Break Your Code)
MD3 didn’t just rename tokens. It restructured the entire color system from the ground up.
In MD2, you had primary, primaryVariant, secondary, and error. Twelve roles total. You could map them in your head and be done.
MD3 introduces 30+ color tokens generated from a single seed color. The scheme includes roles like primaryContainer, onPrimaryContainer, tertiaryContainer, surfaceVariant, surfaceTint, and inverseSurface — each serving a specific semantic purpose in the UI hierarchy. There is no direct mapping from MD2 to MD3. None. Google says so explicitly in their migration docs.
That’s the first thing that breaks legacy code. The second is elevation.
MD2 elevation used drop shadows to communicate depth. Raise a card to 8dp and you get a shadow. Simple, physical, familiar.
MD3 tonal elevation replaces shadows with surface tint overlays. Higher elevation means more of the primary color bleeds into the surface background. A card at elevation 2 has a subtle primary-tinted background. At elevation 5, that tint is visible across the room. Shadows are mostly gone.
If your app has custom elevation handling or shadow drawables, that logic needs rethinking.
Setting Up MD3 with the Right BOM
Before any design work, get the dependencies right. Use the Bill of Materials so version conflicts don’t eat your afternoon:
// build.gradle.kts
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2026.03.00")
implementation(composeBom)
implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose:1.9.0")
}The BOM 2026.03.00 pulls in material3:1.4.0+, which includes the latest MD3 Expressive motion specs announced at Google I/O 2025. Don’t pin an older material3 version manually — let the BOM manage it.
Dynamic Color: Letting the Wallpaper Drive Your Theme
Dynamic color is MD3’s most visible feature. On Android 12+ devices, the system extracts five key colors from the user’s wallpaper and generates a full tonal palette. Your app can opt into that palette instead of shipping a fixed color scheme.
The implementation in Compose is surprisingly clean:
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
darkTheme -> darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
else -> lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
)
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content
)
}The dynamicDarkColorScheme() and dynamicLightColorScheme() functions extract the system palette and return a fully populated ColorScheme with all 30+ tokens filled in. You don’t generate these tokens — the system generates them from the wallpaper.
For devices below Android 12, you fall back to your branded palette. That fallback is where lightColorScheme() with your seed colors kicks in.
Generating a seed-based scheme programmatically:
import androidx.compose.material3.ColorScheme
import com.google.android.material.color.utilities.DynamicScheme
import com.google.android.material.color.utilities.SchemeContent
import com.google.android.material.color.utilities.Hct
fun buildColorScheme(seedHex: Int, isDark: Boolean): ColorScheme {
val hct = Hct.fromInt(seedHex)
val scheme = SchemeContent(hct, isDark, 0.0)
return ColorScheme(
primary = Color(MaterialDynamicColors().primary().getArgb(scheme)),
onPrimary = Color(MaterialDynamicColors().onPrimary().getArgb(scheme)),
primaryContainer = Color(MaterialDynamicColors().primaryContainer().getArgb(scheme)),
// ... remaining 27+ tokens
)
}In practice, use ColorScheme.fromSeed() from the Compose Material3 library — it wraps this process cleanly:
val colorScheme = if (darkTheme) {
darkColorScheme().copy(primary = seedColor) // not ideal
} else {
// Preferred: let Material3 generate from seed
lightColorScheme(primary = seedColor)
}Google’s internal A/B tests showed 30–40% engagement increases in apps that adopted dynamic color, particularly for users who regularly change their wallpaper. The app feels personal rather than generic.
Typography: Five Categories, Fifteen Styles
MD2 had 13 text styles with names like h1 through h6, subtitle1, subtitle2, body1, body2, button, caption, and overline. Memorizing which was which required a cheat sheet.
MD3 flattens this into five categories — Display, Headline, Title, Body, Label — each with three sizes (Large, Medium, Small). Fifteen styles total, all semantically named.
| Category | Use Case |
|---|---|
| Display | Hero text, large marketing headers |
| Headline | Page titles, section headers |
| Title | Card titles, dialog headers |
| Body | Paragraph text, descriptions |
| Label | Buttons, chips, captions |
Defining custom typography in Compose:
val AppTypography = Typography(
displayLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
headlineLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
),
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
labelLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
)
)Then plug it into MaterialTheme:
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content
)Using the scale throughout your UI:
@Composable
fun ProductCard(title: String, description: String) {
Card {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}Notice onSurfaceVariant — that’s an MD3 token with no MD2 equivalent. It’s the recommended color for secondary text on surfaces, automatically adjusted for contrast in both light and dark modes.
Tonal Elevation: Depth Without Shadows
This is where MD2 codebases feel the most friction. Tonal elevation replaces the drop-shadow model entirely for most components.
The mechanic: every surface has a tonalElevation parameter. Instead of casting a shadow, higher elevation overlays more of the surfaceTint color (which defaults to primary) onto the surface background.
@Composable
fun ElevatedCard(content: @Composable () -> Unit) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
tonalElevation = 4.dp, // tint overlay, not shadow
shadowElevation = 0.dp // no drop shadow
) {
content()
}
}For navigation drawers and bottom sheets, MD3 uses tonal elevation to distinguish surfaces from the background — no shadow required. The primary color bleeds into the surface at roughly 8% opacity at 4dp elevation, scaling up with elevation level.
If you’re using custom Box components with manual shadow drawables, audit them. The tonal system only works through Surface and MD3 components — it won’t apply to raw layouts.
Checking tonal colors directly:
val tonalColor = MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp)
// surfaceColorAtElevation is an extension on ColorScheme
// returns surface color with primary tint blended at the appropriate alphaSpring Animations: MD3 Expressive Motion
Announced at Google I/O 2025, MD3 Expressive replaces the rigid easing curves of MD2 with spring-based physics. Gmail, Google Drive, and Meet have all adopted the new motion system.
The key difference is that spring animations don’t have a fixed duration. They respond to velocity — drag something fast, and it overshoots and settles. Drag it slow, and it eases in. The motion feels alive rather than choreographed.
In Compose:
@Composable
fun SpringCard(expanded: Boolean) {
val cardHeight by animateDpAsState(
targetValue = if (expanded) 300.dp else 80.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMediumLow
),
label = "cardHeight"
)
Card(
modifier = Modifier
.fillMaxWidth()
.height(cardHeight)
) {
// content
}
}For shared element transitions — the hero animation pattern — MD3 Expressive uses SharedTransitionLayout:
SharedTransitionLayout {
AnimatedContent(targetState = showDetail) { inDetail ->
if (inDetail) {
DetailScreen(animatedVisibilityScope = this)
} else {
ListScreen(animatedVisibilityScope = this)
}
}
}The MD3 Expressive motion guidelines specify three spring presets for common use cases: EmphasizedDecelerate for elements entering the screen, EmphasizedAccelerate for exits, and Emphasized for within-screen transitions. Each maps to specific damping and stiffness values documented in the Material spec.
Handling the MD2 to MD3 Migration
There’s no automated migration tool. Google’s guidance is clear: treat it as a design re-specification, not a find-and-replace.
The practical path:
1. Isolate theme creation. Move all MaterialTheme setup into a single AppTheme composable. If it’s scattered, fix that first.
2. Map your color roles deliberately. For each MD2 color you used, decide which MD3 token best serves that semantic purpose. Your MD2 primary likely stays primary. Your primaryVariant might become primaryContainer. Your custom surface colors need new MD3 equivalents.
3. Audit elevation usage. Search for elevation = throughout your codebase. Decide for each: does this need tonalElevation, shadowElevation, or both? Many components should drop shadow to zero.
4. Replace MD2 components. Button → Button (same name, new defaults). FloatingActionButton → FloatingActionButton. TopAppBar → TopAppBar. The names are similar but the APIs changed. Check parameters individually.
For apps that need to grow downloads alongside a design refresh, the AppBooster platform tracks how design changes correlate with store metrics — useful when you’re trying to attribute a rating improvement to a specific MD3 rollout.
Real Component Examples: MD3 in Practice
Navigation Bar (was Bottom Navigation):
@Composable
fun AppNavBar(
currentRoute: String,
onNavigate: (String) -> Unit
) {
NavigationBar {
listOf("Home", "Search", "Profile").forEach { route ->
NavigationBarItem(
icon = { Icon(iconFor(route), contentDescription = route) },
label = { Text(route) },
selected = currentRoute == route,
onClick = { onNavigate(route) }
)
}
}
}NavigationBar automatically applies the tonal surface at NavigationBarTokens.ContainerElevation (3dp). No manual tint math needed.
FAB with MD3 Expressive:
@Composable
fun PrimaryFab(onClick: () -> Unit) {
LargeFloatingActionButton(
onClick = onClick,
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
) {
Icon(Icons.Filled.Add, contentDescription = "Add")
}
}primaryContainer rather than primary — a deliberate MD3 choice that gives the FAB lower visual weight than in MD2, while still being clearly actionable.
Chips for filtering:
@Composable
fun FilterChipRow(filters: List<String>, selected: Set<String>, onToggle: (String) -> Unit) {
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(filters) { filter ->
FilterChip(
selected = filter in selected,
onClick = { onToggle(filter) },
label = { Text(filter) },
leadingIcon = if (filter in selected) {
{ Icon(Icons.Filled.Check, contentDescription = null, modifier = Modifier.size(FilterChipDefaults.IconSize)) }
} else null
)
}
}
}FilterChip didn’t exist in MD2. It’s one of several new MD3 components that handles selection state with proper tonal treatment out of the box.
MD3 Expressive: What’s Coming Next
MD3 Expressive, the May 2025 update, goes beyond motion. It introduces:
- Morphing icons — SVG paths animate between states using spring physics
- Shape morphing — Rounded shapes transition fluidly (circle → squircle → rounded rect)
- Adaptive layouts — Components that reshape for different window sizes, not just different screen sizes
- Haptic choreography — Motion and haptic feedback synchronized through a shared timing model
Gmail’s compose button already uses shape morphing — it expands from a circle into a pill when tapped. Meet uses adaptive layouts to reorganize the call grid when you fold a foldable phone.
These features are shipping as part of the material3-adaptive and material3-expressive artifact groups. They’re opt-in at the component level, so you can adopt them incrementally without rewriting existing screens.
The Business Case for Migrating Now
MD3 adoption isn’t just a design hygiene question. Dynamic color produces measurably different engagement than static themes — Google’s own A/B data shows 30–40% higher engagement rates in apps using dynamic color versus identical apps with fixed palettes.
The mechanism isn’t mysterious: when your app reflects the user’s chosen wallpaper, it signals that the app belongs on their phone. Static blue on a terracotta wallpaper screams “installed from the Play Store, forgot to customize.” Dynamic terracotta says “this is mine.”
For apps where ratings and reviews drive discoverability, design quality shows up in the feedback. “Looks outdated” is a real category of negative review. Users may not know what MD3 is, but they know what it feels like. Tools like AppBooster surface that feedback at scale, making it easier to correlate a design update with rating trajectory.
Quick Reference: MD3 Token Cheat Sheet
| Need | MD3 Token |
|---|---|
| Primary action color | colorScheme.primary |
| Primary action background | colorScheme.primaryContainer |
| Text on primary container | colorScheme.onPrimaryContainer |
| Secondary text on surfaces | colorScheme.onSurfaceVariant |
| Background for cards | colorScheme.surfaceVariant |
| Error states | colorScheme.error + colorScheme.errorContainer |
| Disabled elements | colorScheme.onSurface.copy(alpha = 0.38f) |
Migration Is Easier Than Staying Behind
The gap between MD2 and MD3 looks wide from the migration side. From the other side — with a ColorScheme generated from wallpaper, a type scale that actually makes semantic sense, and motion that responds like a physical object — it’s hard to go back.
Start with the BOM update. Then tackle your AppTheme composable. Then work outward to components, one screen at a time. The elevation system will cause the most friction; budget time for that audit.
MD3 Expressive is already shipping in Google’s flagship apps. By the time it reaches Material3 stable, users will expect it. The teams that migrate now won’t be scrambling to catch up then.
Share this article
Build better extensions with free tools
Icon generator, MV3 converter, review exporter, and more — no signup needed.
Related Articles
Android Adaptive Layouts: Your App on Foldables, Tablets, and Everything in Between
300M+ large-screen Android devices. Android 17 mandates adaptive support. Window size classes, canonical layouts, and foldable posture handling.
Android App Onboarding UX: 7 Patterns That Cut Churn by 50%
97.9% of Android users churn by Day 30. These onboarding UX patterns from Duolingo, Headspace, and Notion fight back with data-proven results.
Android Bottom Navigation vs Navigation Drawer: How to Choose the Right Pattern
Bottom nav for 3-5 destinations, drawer for 6+. Material Design 3 guidelines, thumb zones, and real examples from Gmail, Instagram, and Maps.