Material Design 3 for Android: Dynamic Color, Typography, and the New Design Language

AppBooster Team · · 12 min read
Colorful abstract material design shapes and gradients

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.

Material Design 3 color system visualization showing tonal palettes


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.

CategoryUse Case
DisplayHero text, large marketing headers
HeadlinePage titles, section headers
TitleCard titles, dialog headers
BodyParagraph text, descriptions
LabelButtons, 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.

Typography scale visualization for Material Design 3


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 alpha

Spring 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. ButtonButton (same name, new defaults). FloatingActionButtonFloatingActionButton. TopAppBarTopAppBar. 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.

Android Studio with Material Design 3 theme editor


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.

Abstract colorful motion blur representing spring animations


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

NeedMD3 Token
Primary action colorcolorScheme.primary
Primary action backgroundcolorScheme.primaryContainer
Text on primary containercolorScheme.onPrimaryContainer
Secondary text on surfacescolorScheme.onSurfaceVariant
Background for cardscolorScheme.surfaceVariant
Error statescolorScheme.error + colorScheme.errorContainer
Disabled elementscolorScheme.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