Jetpack Compose makes it easy to give your app a consistent look and feel by applying themes. You can customize Compose's implementation of Material Design to fit your product's brand. If that doesn't suit your needs, you can build a custom design system using Compose's public APIs.
Application-wide theming
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 comprises 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.
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 support multiple themes, like dark theme.
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.
Figure 2. 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, that sets
onPrimary
as
the content color. If you set a non-theme background color, you should also
specify a sensible content color. Use
LocalContentColor
to retrieve the current content color which contrasts with the current
background.
Content Alpha
Often we want to vary how much we emphasize content to communicate importance and provide visual hierarchy. Material Design recommends employing different levels of opacity to convey these 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
.
Child composables can use this value, 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. Note that MaterialTheme
defaults
LocalContentAlpha
to ContentAlpha.high
.
// 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(/*...*/)
}
Figure 3. Apply different levels of emphasis to text to visually communicate the information hierarchy.
Dark theme
In Compose, you implement light and dark themes by providing different sets of
Colors
to the MaterialTheme
composable, and consuming colors through the
theme:
@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.
When implementing a dark theme, you can check if the current Colors
are light
or dark:
val isLightTheme = MaterialTheme.colors.isLight
This value is set by the
lightColors()
and
darkColors()
builder functions.
In Material, surfaces in dark themes with higher elevations receive elevation
overlays, which
lightens their background. These overlays are implemented automatically by the
Surface
composable when using dark colors:
Surface(
elevation = 2.dp,
color = MaterialTheme.colors.surface, // color will be adjusted for elevation
/*...*/
) { /*...*/ }
Figure 4. The Cards and Bottom Navigation are surface
colored like the
background, but because they are at a higher elevation, their color is slightly
lighter.
Extending Material colors
Compose closely models Material’s color theming to make it simple and type-safe to follow material guidelines. If you need to extend the color set, you can either implement your own color system as shown below, or add extensions:
@Composable
val Colors.snackbarAction: Color
get() = if (isLight) Red300 else Red700
Typography
Material defines a type system, encouraging you to use a small number of semantically-named styles.
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 font throughout, specify the defaultFontFamily
parameter and omit
the fontFamily
of any TextStyle
elements:
val typography = Typography(defaultFontFamily = Rubik)
MaterialTheme(typography = typography, /*...*/)
Using text styles
Retrieve TextStyle
from the theme, as shown in this example:
Text(
text = "Subtitle2 styled",
style = MaterialTheme.typography.subtitle2
)
Figure 5. 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.
Compose implements the shape system with the
Shapes
class, which lets
you specify a
CornerBasedShape
for each category:
val Shapes = Shapes(
small = RoundedCornerShape(percent = 50),
medium = RoundedCornerShape(0f),
large = CutCornerShape(
topStart = 16.dp,
topEnd = 0.dp,
bottomStart = 16.dp,
bottomEnd = 0.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
ModalDrawerLayout
defaults to large — see the shape
scheme
reference for the complete mapping.
Using shapes
Retrieve the shapes from the theme:
Surface(
shape = MaterialTheme.shapes.medium, /*...*/
) {
/*...*/
}
Figure 6. Use shapes to express brand or state.
Component styles
There is no explicit concept of component styles in Compose, as you provide this functionality by creating your own composables. 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 LoginButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
Button(
colors = ButtonConstants.defaultButtonColors(
backgroundColor = MaterialTheme.colors.secondary
),
onClick = onClick,
modifier = modifier,
content = content
)
}
Custom design systems
While Material is our recommended design system and Jetpack Compose ships an implementation of Material, you are not restricted to using it. It’s entirely possible to create your own design system in the same manner; Material is built entirely on public APIs which you can use to achieve this.
A complete description of how to build a custom design system is beyond the scope of this document, but see the following resources:
- MaterialTheme source code
- CompositionLocal
- Providers
- Jetsnack sample application, which implements a custom color system
Learn more
To learn more, try the Jetpack Compose Theming codelab.