Material Theming in Compose

Jetpack Compose offers an implementation of Material Design, a comprehensive design system for creating digital interfaces. The Material Design components (buttons, cards, switches, and so on) are built on top of Material Theming, which is a systematic way to customize Material Design to better reflect your product’s brand. A Material Theme contains color, typography and shape attributes. When you customize these attributes, your changes are automatically reflected in the components you use to build your app.

Jetpack Compose implements these concepts with the MaterialTheme composable:

MaterialTheme(
    colors = …,
    typography = …,
    shapes = …
) {
    // app content
}

Configure the parameters you pass to MaterialTheme to theme your application.

Two contrasting screenshots. The first uses default MaterialTheme styling,
the second screenshot uses modified styling.

Figure 1. The first screenshot shows an app that does not configure MaterialTheme, and so it uses default styling. The second screenshot shows an app that passes parameters to MaterialTheme to customize the styling.

Color

Colors are modelled in Compose with the Color class, a simple data-holding class.

val Red = Color(0xffff0000)
val Blue = Color(red = 0f, green = 0f, blue = 1f)

While you can organize these however you like (as top-level constants, within a singleton, or defined inline), we strongly recommend specifying colors in your theme and retrieving the colors from there. This approach makes it possible to easily support dark theme and nested themes.

Example of theme's color palette

Figure 2. The Material color system.

Compose provides the Colors class to model the Material color system. Colors provides builder functions to create sets of light or dark colors:

private val Yellow200 = Color(0xffffeb46)
private val Blue200 = Color(0xff91a4fc)
// ...

private val DarkColors = darkColors(
    primary = Yellow200,
    secondary = Blue200,
    // ...
)
private val LightColors = lightColors(
    primary = Yellow500,
    primaryVariant = Yellow400,
    secondary = Blue700,
    // ...
)

Once you have defined your Colors you can pass them to a MaterialTheme:

MaterialTheme(
    colors = if (darkTheme) DarkColors else LightColors
) {
    // app content
}

Using theme colors

You can retrieve the Colors provided to the MaterialTheme composable by using MaterialTheme.colors.

Text(
    text = "Hello theming",
    color = MaterialTheme.colors.primary
)

Surface and content color

Many components accept a pair of color and content color:

Surface(
    color: Color = MaterialTheme.colors.surface,
    contentColor: Color = contentColorFor(color),
    // ...

TopAppBar(
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    // ...

This enables you to not only set the color of a composable, but also to provide a default color for the content, the composables contained within it. Many composables use this content color by default. For example, Text bases its color on its parent's content color, and Icon uses that color to set its tint.

Two examples of the same banner, with different colors

Figure 3. Setting different background colors produces different text and icon colors.

The contentColorFor() method retrieves the appropriate "on" color for any theme colors. For example, if you set a primary background color on Surface, it uses this function to set onPrimary as the content color. If you set a non-theme background color, you should also specify an appropriate content color. Use LocalContentColor to retrieve the preferred content color for the current background, at a given position in the hierarchy.

Content alpha

Often you want to vary how much you emphasize content to communicate importance and provide visual hierarchy. The Material Design text legibility recommendations advise employing different levels of opacity to convey different importance levels.

Jetpack Compose implements this via LocalContentAlpha. You can specify a content alpha for a hierarchy by providing a value for this CompositionLocal. Nested composables can use this value to apply the alpha treatment to their content. For example, Text and Icon by default use the combination of LocalContentColor adjusted to use LocalContentAlpha. Material specifies some standard alpha values (high, medium, disabled) which are modelled by the ContentAlpha object.

// By default, both Icon & Text use the combination of LocalContentColor &
// LocalContentAlpha. De-emphasize content by setting content alpha
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
    Text(/*...*/)
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
    Icon(/*...*/)
    Text(/*...*/)
}

To learn more about CompositionLocal, check out the Locally scoped data with CompositionLocal guide.

Screenshot of an article title, showing different levels of text
emphasis

Figure 4. Apply different levels of emphasis to text to visually communicate the information hierarchy. The first line of text is the title and has the most important information, and thus uses ContentAlpha.high. The second line contains less-important metadata, and thus uses ContentAlpha.medium.

Dark theme

In Compose, you implement light and dark themes by providing different sets of Colors to the MaterialTheme composable:

@Composable
fun MyTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors,
        /*...*/
        content = content
    )
}

In this example, MaterialTheme is wrapped in its own composable function, which accepts a parameter that specifies whether to use a dark theme or not. In this case, the function gets the default value for darkTheme by querying the device theme setting.

You can use code like this to check if the current Colors are light or dark:

val isLightTheme = MaterialTheme.colors.isLight
Icon(
    painterResource(
        id = if (isLightTheme) {
          R.drawable.ic_sun_24dp
        } else {
          R.drawable.ic_moon_24dp
        }
    ),
    contentDescription = "Theme"
)

Elevation overlays

In Material, surfaces in dark themes with higher elevations receive elevation overlays, which lightens their background. The higher a surface's elevation (raising it closer to an implied light source), the lighter that surface becomes.

These overlays are applied automatically by the Surface composable when using dark colors, and for any other Material composable which uses a surface:

Surface(
    elevation = 2.dp,
    color = MaterialTheme.colors.surface, // color will be adjusted for elevation
    /*...*/
) { /*...*/ }

Screenshot of an app, showing the subtly different colors used for elements
at different elevation levels

Figure 5. The cards and bottom navigation are both using the surface color as their background. Since the cards and bottom navigation are at different elevation levels above the background, they have slightly different colors–the cards are lighter than the background and the bottom navigation is lighter than the cards.

For custom scenarios that don’t involve a Surface, use LocalElevationOverlay, a CompositionLocal containing the ElevationOverlay used by Surface components:

// Elevation overlays
// Implemented in Surface (and any components that use it)
val color = MaterialTheme.colors.surface
val elevation = 4.dp
val overlaidColor = LocalElevationOverlay.current?.apply(
    color, elevation
)

To disable elevation overlays, provide null at the desired point in a composable hierarchy:

MyTheme {
    CompositionLocalProvider(LocalElevationOverlay provides null) {
        // Content without elevation overlays
    }
}

Limited color accents

Material recommends applying limited color accents for dark themes by preferring the use of the surface color over the primary color in most cases. Material composables like TopAppBar and BottomNavigation implement this behavior by default.

Figure 6. Material dark theme with limited color accents. The top app bar uses the primary color in light theme, and surface color in dark theme.

For custom scenarios, use the primarySurface extension property:

Surface(
    // Switches between primary in light theme and surface in dark theme
    color = MaterialTheme.colors.primarySurface,
    /*...*/
) { /*...*/ }

Typography

Material defines a type system, encouraging you to use a small number of semantically-named styles.

Example of several different typefaces in various styles

Figure 7. The Material type system.

Compose implements the type system with the Typography, TextStyle, and font-related classes. The Typography constructor offers defaults for each style so you can omit any you don’t want to customize:

val Rubik = FontFamily(
    Font(R.font.rubik_regular),
    Font(R.font.rubik_medium, FontWeight.W500),
    Font(R.font.rubik_bold, FontWeight.Bold)
)

val MyTypography = Typography(
    h1 = TextStyle(
        fontFamily = Rubik,
        fontWeight = FontWeight.W300,
        fontSize = 96.sp
    ),
    body1 = TextStyle(
        fontFamily = Rubik,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    )
    /*...*/
)
MaterialTheme(typography = MyTypography, /*...*/)

If you want to use the same typeface throughout, specify the defaultFontFamily parameter and omit the fontFamily of any TextStyle elements:

val typography = Typography(defaultFontFamily = Rubik)
MaterialTheme(typography = typography, /*...*/)

Using text styles

TextStyles are accessed via MaterialTheme.typography. Retrieve the TextStyles like so:

Text(
    text = "Subtitle2 styled",
    style = MaterialTheme.typography.subtitle2
)

Screenshot showing a mixture of different typefaces for different purposes

Figure 8. Use a selection of typefaces and styles to express your brand.

Shape

Material defines a shape system, allowing you to define shapes for large, medium, and small components.

Shows a variety of Material Design shapes

Figure 9. The Material shape system.

Compose implements the shape system with the Shapes class, which lets you specify a CornerBasedShape for each size category:

val Shapes = Shapes(
    small = RoundedCornerShape(percent = 50),
    medium = RoundedCornerShape(0f),
    large = CutCornerShape(
        topStart = 16.dp,
        topEnd = 0.dp,
        bottomEnd = 0.dp,
        bottomStart = 16.dp
    )
)

MaterialTheme(shapes = Shapes, /*...*/)

Many components use these shapes by default. For example, Button, TextField, and FloatingActionButton default to small, AlertDialog defaults to medium, and ModalDrawer defaults to large — see the shape scheme reference for the complete mapping.

Using shapes

Shapes are accessed via MaterialTheme.shapes. Retrieve the Shapes with code like this:

Surface(
    shape = MaterialTheme.shapes.medium, /*...*/
) {
    /*...*/
}

Screenshot of an app that uses Material shapes to convey what state an element is in

Figure 10. Use shapes to express brand or state.

Default styles

There is no equivalent concept in Compose of default styles from Android Views. You can provide similar functionality by creating your own ‘overload’ composable functions that wrap Material components. For example, to create a style of button, wrap a button in your own composable function, directly setting the parameters you wish to alter, and exposing others as parameters to the containing composable.

@Composable
fun MyButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

Theme overlays

You can achieve the equivalent of theme overlays from Android Views in Compose, by nesting MaterialTheme composables. Because MaterialTheme defaults the colors, typography, and shapes to the current theme value, if a theme only sets one of those parameters, the other parameters keep their default values.

Furthermore, when migrating View-based screens to Compose, watch out for usages of the android:theme attribute. It's likely you need a new MaterialTheme in that part of the Compose UI tree.

In the Owl sample, the details screen uses a PinkTheme for most of the screen, and then a BlueTheme for the related section. See screenshot and code below.

Figure 11. Nested themes in the Owl sample.

@Composable
fun DetailsScreen(/* ... */) {
    PinkTheme {
        // other content
        RelatedSection()
    }
}

@Composable
fun RelatedSection(/* ... */) {
    BlueTheme {
        // content
    }
}

Component states

Material components that can be interacted with (clicked, toggled, etc.) can be in different visual states. States include enabled, disabled, pressed, etc.

Composables often have an enabled parameter. Setting it to false prevents interaction, and changes properties like color and elevation to visually convey the component state.

Figure 12. Button with enabled = true (left) and enabled = false (right).

In most cases you can rely on defaults for values like color and elevation. Should you wish to configure values used in different states, there are classes and convenience functions available. See the button example below:

Button(
    onClick = { /* ... */ },
    enabled = true,
    // Custom colors for different states
    colors = ButtonDefaults.buttonColors(
        backgroundColor = MaterialTheme.colors.secondary,
        disabledBackgroundColor = MaterialTheme.colors.onBackground
            .copy(alpha = 0.2f)
            .compositeOver(MaterialTheme.colors.background)
        // Also contentColor and disabledContentColor
    ),
    // Custom elevation for different states
    elevation = ButtonDefaults.elevation(
        defaultElevation = 8.dp,
        disabledElevation = 2.dp,
        // Also pressedElevation
    )
) { /* ... */ }

Figure 13. Button with enabled = true (left) and enabled = false (right), with adjusted color and elevation values.

Ripples

Material Components use ripples to indicate they’re being interacted with. If you’re using MaterialTheme in your hierarchy, a Ripple will be used as the defaultIndication inside modifiers such as clickable and indication.

In most cases you can rely on the default Ripple. Should you wish to configure their appearance, you can use RippleTheme to change properties like color and alpha.

You can extend RippleTheme and make use of the defaultRippleColor and defaultRippleAlpha utility functions. You can then provide your custom ripple theme in your hierarchy using LocalRippleTheme:

@Composable
fun MyApp() {
  MaterialTheme {
    CompositionLocalProvider(
      LocalRippleTheme provides SecondaryRippleTheme
    ) {
      // App content
    }
  }
}

@Immutable
private object SecondaryRippleTheme : RippleTheme {
  @Composable
  override fun defaultColor() = RippleTheme.defaultRippleColor(
    contentColor = MaterialTheme.colors.secondary,
    lightTheme = MaterialTheme.colors.isLight
  )

  @Composable
  override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
    contentColor = MaterialTheme.colors.secondary,
    lightTheme = MaterialTheme.colors.isLight
  )
}

alt_text

Figure 14. Buttons with different ripple values provided via RippleTheme.

Material Design 3 and Material You

Jetpack Compose offers an implementation of Material Design 3, the next evolution of Material Design. Material 3 includes updated theming and components and Material You personalization features like dynamic color, and is designed to be cohesive with the new Android 12 visual style and system UI.

Begin by adding the new Compose Material 3 dependency to your build.gradle files:

implementation "androidx.compose.material3:material3:material3_version"

An M3 theme contains color scheme and typography values, with updates to shapes coming soon. When you customize these values, your changes are automatically reflected in the M3 components you use to build your app.

Jetpack Compose implements these concepts with the new M3 MaterialTheme composable:

MaterialTheme(
    colorScheme = …,
    typography = …
    // Updates to shapes coming soon
) {
    // M3 app content
}

Configure the parameters you pass to the M3 MaterialTheme to theme your application.

Figure 15. The first two screenshots show an app that does not configure the M3 MaterialTheme, so it uses default styling. The second two screenshots show an app that passes parameters to MaterialTheme to customize the styling.

Color scheme

Material Design 3 separates colors into named color “slots” — such as ‘primary’, ‘background’, and ‘error’ — which are used by Material 3 components. Together, these slots form a color scheme. The color values used by each slot are drawn from a set of tonal palettes, selected to meet accessibility requirements (for example, the ‘primary’ slot is guaranteed to contrast with the ‘on primary’ slot). The color schemes feature new default baseline colors for both light and dark themes.

Figure 16. Material 3 tonal palettes and color schemes, with baseline color values.

Compose provides the ColorScheme class to model the Material 3 color scheme. ColorScheme provides builder functions to create a light or dark color scheme:

private val Blue40 = Color(0xff1e40ff​​)
private val DarkBlue40 = Color(0xff3e41f4)
private val Yellow40 = Color(0xff7d5700)
// Remaining colors from tonal palettes

private val LightColorScheme = lightColorScheme(
    primary = Blue40,
    secondary = DarkBlue40,
    tertiary = Yellow40,
    // error, primaryContainer, onSecondary, etc.
)
private val DarkColorScheme = darkColorScheme(
    primary = Blue80,
    secondary = DarkBlue80,
    tertiary = Yellow80,
    // error, primaryContainer, onSecondary, etc.
)

Once you have defined your ColorScheme you can pass it to a M3 MaterialTheme:

val darkTheme = isSystemInDarkTheme()
MaterialTheme(
    colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
) {
    // M3 app content
}

Generating color schemes

While you can create a custom ColorScheme manually, it’s often easier to generate one using source colors from your brand. The Material Theme Builder tool allows you to do this, and optionally export Compose theming code.

Figure 17. Material 3 tonal palettes and color schemes, with custom color values, generated by the Material Theme Builder tool.

Dynamic color schemes

Dynamic color is the key part of Material You, in which an algorithm derives custom colors from a user’s wallpaper to be applied to their apps and system UI. This color palette is used as the starting point to generate a complete light and dark color scheme.

Dynamic color is available on Android 12 and above. If dynamic color is available, you can set up a dynamic ColorScheme. If not, you can fall back to using a custom light or dark ColorScheme.

ColorScheme provides builder functions to create a dynamic light or dark color scheme:

// Dynamic color is available on Android 12+
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when {
    dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
    dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
    darkTheme -> DarkColorScheme
    else -> LightColorScheme
}

Figure 18. The first two screenshots show an app that is using a dynamic ColorScheme based on a red wallpaper. The second two screenshots show an app that is using a dynamic ColorScheme based on a blue wallpaper.

Using color scheme colors

You can retrieve the ColorScheme provided to the M3 MaterialTheme composable by using MaterialTheme.colorScheme:

Text(
    text = "Hello M3 theming",
    color = MaterialTheme.colorScheme.tertiary
)

Typography

Material Design 3 defines a type scale, including text styles that have been adapted from Material Design 2. The naming and grouping have been simplified to: display, headline, title, body, and label, with large, medium, and small sizes for each.

Figure 19. The Material 3 type scale vs. the Material 2 type scale.

Compose provides the new M3 Typography class — along with the existing TextStyle and font-related classes — to model the Material 3 type scale:

val KarlaFontFamily = FontFamily(
    Font(R.font.karla_regular),
    Font(R.font.karla_bold, FontWeight.Bold)
)

val AppTypography = Typography(
    bodyLarge = TextStyle(
        fontFamily = KarlaFontFamily,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.15.sp
    ),
    // titleMedium, labelSmall, etc.
)

Once you have defined your Typography you can pass it to a M3 MaterialTheme:

MaterialTheme(
    typography = AppTypography
) {
    // M3 app content
}

Using text styles

You can retrieve the Typography provided to the M3 MaterialTheme composable by using MaterialTheme.typography:

Text(
    text = "Hello M3 theming",
    style = MaterialTheme.typography.bodyLarge
)

Elevation

Material 3 represents elevation mainly using tonal color overlays. This is a new way to distinguish containers and surfaces from each other — increasing tonal elevation uses a more prominent tone — in addition to shadows.

Elevation overlays in dark theme have also changed to tonal color overlays in Material 3.

The overlay color comes from the primary color slot.

Figure 20. Material 3 elevation vs. Material 2 elevation, in both light and dark theme.

The M3 Surface — the backing composable behind most M3 components — includes support for both tonal and shadow elevation:

Surface(
    tonalElevation = 16.dp,
    shadowElevation = 16.dp
) {
    // Surface content
}

System UI

Some aspects of Material You come from the new Android 12 visual style and system UI. Two key areas where there are changes are ripple and overscroll. No additional work is required to implement these changes.

Ripple

Ripple now uses a subtle sparkle to illuminate surfaces when pressed. Compose Material Ripple uses a platform RippleDrawable under the hood on Android, so sparkle ripple is available on Android 12 and above for all Material components.

Figure 21. Android 12 ripple vs. pre-Android 12 ripple.

Overscroll

Overscroll now uses a stretch effect at the edge of scrolling containers. Stretch overscroll is on by default in scrolling container composables — for example, LazyColumn, LazyRow, and LazyVerticalGrid — in Compose Foundation 1.1.0 and above, regardless of API level.

Figure 22. Stretch overscroll.

Learn more

To learn more about Material Theming in Compose, consult the following additional resources.

Codelabs

Videos