Droid Manual // Android Knowledge Base

Droid Manual

Material Design 3 Implementation

October 31, 2026 · 11 min read

Introduction

Material Design 3 (often called Material You) represents a significant paradigm shift from Material Design 2, moving from static, hand-crafted themes to a dynamic, personalization-first approach. Google introduced this design language alongside Android 12, bringing system-wide color extraction from your wallpaper, adaptive layouts, and a richer tonal palette system built on the HCT color space. For Android developers, embracing Material 3 means rewriting how they think about themes, colors, and component styling in both View-based and Compose-based projects. This tutorial walks through everything you need to migrate your app or build new features on Material 3, covering dynamic color extraction, tonal palettes, Compose MaterialTheme setup, granular component customization, and the key differences that trip up developers coming from Material 2. Whether you are starting fresh or migrating a legacy codebase, the guidance here will give you a production-ready foundation.

Setting Up Material 3 in Your Project

The first step toward Material 3 is ensuring your project depends on the correct libraries and that your theme inherits from the right base classes. Material 3 ships as a separate artifact from Material 2, so you must explicitly declare the dependency. For Jetpack Compose projects, you pull in material3 alongside material-icons-extended if you need the expanded icon set. For View-based projects, you depend on the material AndroidX library at version 1.11.0 or higher, which includes Material 3 components like FilledButton, Chip, and updated card implementations. In your app-level build.gradle.kts, add the dependency and verify your compile SDK is at least 33 so the new APIs are available.

dependencies {
    implementation("androidx.compose.material3:material3:1.3.1")
    implementation("androidx.compose.material:material-icons-extended:1.7.5")
    implementation("com.google.android.material:material:1.12.0")
}

This Gradle configuration pulls in the latest stable Material 3 Compose library, the extended icon set for icons that exist only in Material 3, and the View-based Material library that supports both Material 2 and Material 3 widgets in the same project. The versions shown are current as of mid-2024; always check the Maven repository for newer releases. With these dependencies in place, your project is ready to define a Material 3 theme.

Creating a Material 3 Color Theme

Material 3 organizes colors around the concepts of primary, secondary, tertiary, surface, and background roles, each with a full tonal range from 0 (white) to 100 (black). Unlike Material 2, where you typically defined a handful of color resources, Material 3 expects a richer palette because components derive their appearance directly from these semantic color roles. The recommended approach is to define your colors in a Kotlin object using Color constants from androidx.compose.ui.graphics, then wrap them in a LightColors or DarkColors instance. When you generate a theme with dynamicColor = false, you provide the tonal palette explicitly; when dynamicColor = true, the system extracts colors from the user's wallpaper and overrides many of your values at runtime.

import androidx.compose.ui.graphics.Color

val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)

val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

private val md_color_scheme_light = lightColorScheme(
    primary = Purple40,
    secondary = PurpleGrey40,
    tertiary = Pink40,
    primaryContainer = Purple80,
    secondaryContainer = PurpleGrey80,
    tertiaryContainer = Pink80,
    background = Color(0xFFFFFBFE),
    surface = Color(0xFFFFFBFE),
    onPrimary = Color.White,
    onSecondary = Color.White,
    onSurface = Color(0xFF1C1B1F),
)

private val md_color_scheme_dark = darkColorScheme(
    primary = Purple80,
    secondary = PurpleGrey80,
    tertiary = Pink80,
    primaryContainer = Purple40,
    secondaryContainer = PurpleGrey40,
    tertiaryContainer = Pink40,
    background = Color(0xFF1C1B1F),
    surface = Color(0xFF1C1B1F),
    onPrimary = Color(0xFF381E72),
    onSecondary = Color(0xFF333133),
    onSurface = Color(0xFFE6E1E5),
)

This Kotlin snippet defines two complete color schemes, one for light mode and one for dark mode, using the semantic color roles that Material 3 expects. Notice how the light scheme uses lighter container colors that pair with darker primary hues, while the dark scheme inverts that relationship. The lightColorScheme and darkColorScheme constructors accept all of the named parameters shown, and any you omit fall back to sensible defaults—though relying on defaults in production is rarely advisable because you lose control over your brand identity. These color objects become the foundation for the MaterialTheme composable you will declare next.

Enabling Dynamic Color

Dynamic color is the signature feature of Material 3, and it gives your app the ability to automatically generate a harmonious color palette from the user's wallpaper. To use it, you pass dynamicColor = true into the MaterialTheme composable, and the framework calls dynamicDarkColorScheme or dynamicLightColorScheme depending on the current theme mode. This feature requires Android 12 (API level 32) as a minimum, but you can also enable it on earlier versions by depending on material3-android, which backports the dynamic color extraction logic. Inside the MaterialTheme block, you reference colorScheme rather than a hardcoded scheme, and every Material 3 component that respects the theme will automatically pick up the generated colors. If dynamic color is unavailable—because the device runs an older Android version or the user has disabled it—the scheme gracefully falls back to whatever you provide.

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S -> {
            if (darkTheme) dynamicDarkColorScheme(LocalContext.current)
            else dynamicLightColorScheme(LocalContext.current)
        }
        darkTheme -> md_color_scheme_dark
        else -> md_color_scheme_light
    }

    androidx.compose.material3.MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

In this composable, the conditional branching first checks whether dynamic color is requested and whether the device runs Android 12 or higher. On supported devices, the framework extracts colors from the wallpaper and returns a live color scheme that updates when the wallpaper changes. On older devices or when the user opts out, the composable falls back to the static schemes defined earlier. The when expression guarantees that exactly one branch is taken, and because MaterialTheme wraps all child composables, every button, card, and surface in the hierarchy automatically inherits the resolved color scheme without any additional code.

Configuring Typography and Shape

Beyond colors, Material 3 introduces an updated typography scale and a more generous default shape system. The Material 3 typography specification defines four text styles—Display, Headline, Title, Body, and Label—each with distinct size, weight, and letter-spacing values that create a clear visual hierarchy. Material 3 also shifts from the 4dp rounded corners of Material 2 to larger, more varied corner radii: small components use 8dp, medium ones 12dp, and large surfaces up to 28dp. The default Typography and Shapes objects provided by the material3 library already follow these specifications, so in most cases you only need to pass them explicitly into MaterialTheme to adopt the full Material 3 look.

import androidx.compose.material3.Typography
import androidx.compose.material3.Shapes
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.compose.material3.MaterialTheme

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
    ),
    titleLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Medium,
        fontSize = 22.sp,
        lineHeight = 28.sp
    ),
    bodyLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp
    ),
    labelLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
        lineHeight = 20.sp
    )
)

val AppShapes = Shapes(
    extraSmall = androidx.compose.foundation.shape.RoundedCornerShape(4.dp),
    small = androidx.compose.foundation.shape.RoundedCornerShape(8.dp),
    medium = androidx.compose.foundation.shape.RoundedCornerShape(12.dp),
    large = androidx.compose.foundation.shape.RoundedCornerShape(16.dp),
    extraLarge = androidx.compose.foundation.shape.RoundedCornerShape(28.dp)
)

This snippet defines custom Typography and Shapes objects that follow the Material 3 spec. The displayLarge style uses negative letter-spacing, which is a hallmark of Material 3 display text and distinguishes it from the default Android typography. Each TextStyle is fully specified with font family, weight, size, line height, and letter spacing, giving you pixel-perfect control. The Shapes object maps the five standard size tiers to the rounded corner radii recommended by the Material 3 spec, and these shapes are referenced by components like Scaffold, Card, and ModalBottomSheet when you pass AppShapes into MaterialTheme. Using these explicit objects ensures consistency even if Google updates the library defaults later.

Customizing Individual Components

While the global MaterialTheme configuration handles most styling, there are cases where you need to override individual components without breaking the overall theme. Material 3 provides modifier-based APIs and component-specific parameters that let you adjust elevation, size, and color on a per-component basis. For example, IconButton accepts an interactionSource for ripple feedback and a containerColor parameter, while Scaffold lets you override the topBar and floatingActionButton shapes independently of the global shape scheme. You can also create custom composable wrappers that apply your brand-specific adjustments while still reading from the theme's color and typography objects, ensuring your overrides remain responsive to both light and dark modes and dynamic color changes.

import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun PrimaryActionButton(
    onClick: () -> Unit,
    content: @Composable () -> Unit
) {
    FilledTonalButton(
        onClick = onClick,
        colors = ButtonDefaults.filledTonalButtonColors(
            containerColor = Color(0xFF6750A4),
            contentColor = Color.White
        ),
        shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp),
        elevation = ButtonDefaults.filledTonalButtonElevation(
            defaultElevation = 2.dp,
            pressedElevation = 6.dp
        ),
        content = content
    )
}

This composable wraps a FilledTonalButton with a consistent brand style that you can reuse across your app. By specifying containerColor and contentColor directly, you ensure the button always uses the correct contrast pair regardless of what the theme provides, though in a fully themed app you would typically derive these from colorScheme.primary and colorScheme.onPrimary instead. The shape parameter overrides the default rounded corner for this button only, and the elevation configuration defines both the resting and pressed states, creating a tactile press feedback that matches Material 3's elevation semantics. Components outside this wrapper continue to use the default theme shapes and elevations, so your customization is scoped precisely where you need it.

Migrating from Material 2 to Material 3

Migrating from Material 2 to Material 3 involves more than swapping a dependency version; it requires rethinking how your colors, shapes, and components are structured. In Material 2, you extended Theme.AppCompat.Light.DarkActionBar and defined colors in colors.xml; in Material 3, you define a Kotlin ColorScheme and pass it into MaterialTheme. Many Material 2 widgets like FloatingActionButton and Card still exist but now have Material 3 equivalents with different default appearances—for instance, MaterialCardView uses the new tonal palette by default. A critical breaking change is that Material 3 components read from the Compose MaterialTheme context rather than the Android Context theme, so any custom style you applied via attrs.xml in Material 2 will no longer take effect. The migration path is best approached incrementally: upgrade the dependency first, enable dynamic color on devices that support it, then systematically replace deprecated Material 2 composables with their Material 3 counterparts.

import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.DrawerState
import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.RevealScrollableHeader
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MigratedScreen() {
    val drawerState = rememberDrawerState(initialValue = DrawerState.Closed)
    val scope = rememberCoroutineScope()

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Migrated to Material 3") },
                navigationIcon = {
                    IconButton(onClick = { scope.launch { drawerState.open() } }) {
                        Icon(
                            imageVector = /* your icon */ androidx.compose.material.icons.Icons.Filled.Menu,
                            contentDescription = "Open navigation"
                        )
                    }
                }
            )
        },
        drawerContent = { /* drawer content */ }
    ) { padding ->
        // Your migrated screen content here
    }
}

This composable demonstrates a typical migrated screen that replaces the old DrawerLayout plus AppBarLayout pattern from Material 2 with the Material 3 Scaffold and TopAppBar. The rememberDrawerState API manages the drawer's open and closed state declaratively, and rememberCoroutineScope provides the coroutine needed to animate the drawer open when the navigation icon is tapped. Notice how the Scaffold's topBar parameter replaces the old XML-based app bar declaration, and how the padding lambda ensures content respects the app bar's height. Every element inside this scaffold automatically inherits the Material 3 color scheme and typography, so the visual upgrade is immediate once the dependency is updated and the theme is applied.

Common Pitfalls

Even with a solid understanding of Material 3, developers commonly run into issues that can undermine the user experience. One frequent mistake is hardcoding color values instead of reading from colorScheme, which causes your UI to break the moment dynamic color is enabled or the user switches to dark mode—suddenly your manually specified blue background will sit on top of a dark surface with poor contrast. Another pitfall is ignoring the elevation system; Material 3 ties elevation to tonal depth rather than drop shadows, so applying old Material 2 shadow values creates visual inconsistency. Developers also sometimes confuse primaryContainer with primary, using the container color where the primary color is expected and producing overly saturated or muted results. A third common error is neglecting to opt into ExperimentalMaterial3Api when using newer components like TopAppBar with scrollable headers, which leads to compilation errors that stall migration progress. Finally, mixing Material 2 widgets like MaterialButton with Material 3 composables in the same screen creates a jarring hybrid appearance that defeats the purpose of the migration, so it is important to audit your UI layer by layer.

Conclusion

Material Design 3 is not merely a视觉 update; it is a fundamental rethinking of how Android apps express identity, adapt to individual users, and maintain visual coherence across light and dark modes. By setting up the correct dependencies, defining semantic color schemes, enabling dynamic color where possible, and adopting the new component APIs, you can bring your app up to date with minimal visual regression. The migration from Material 2 is incremental and safe when done component by component, and the customization layer in Material 3 gives you more granular control than the old XML-based theming system ever did. Keep your colors derived from the theme, respect the elevation and shape scale, and your app will look polished on every device and every wallpaper.